[Impeller] construct text frames on UI thread. (flutter/engine#45418)

Conversion of SkTextBlobs to impeller::TextFrame objects is one of the most expensive operations in display list dispatching. While the rest of the engine and framework makes a reasonable attempt to cache the SkTextBlobs generated during paragraph construction, the design of the dl dispatcher means that these the Impeller backend will always reconstruct all text frames on each frame - even if the display list/picture that contained those text frames was unchanged.

Removing this overhead is one of the goals of https://github.com/flutter/engine/pull/45386 , however this patch is also fairly risky and will be difficult to land. As a more incremental solution, we can instead construct the impeller::TextFrame objects when performing paragraph painting and record them in the display list. This both moves the text frame construction to the UI thread and allows the framework/engine to cache unchanged text frames.

This also does not conflict with the dl_aiks_canvas patch directly, and is fine to land before or after it does. (though I'd argue we should land this first).

To compare the current performance levels, I ran the complex_layout_scroll perf test, since this is fairly text filled. On a Pixel 6 pro.   Across several runs this is a fairly consistent ~1ms raster time improvement.

### Skia
```
  "average_frame_build_time_millis": 1.497333333333333,
  "90th_percentile_frame_build_time_millis": 2.038,
  "99th_percentile_frame_build_time_millis": 17.686,
  "worst_frame_build_time_millis": 23.095,
  "missed_frame_build_budget_count": 3,
  "average_frame_rasterizer_time_millis": 5.5078589743589745,
  "stddev_frame_rasterizer_time_millis": 2.226343414420338,
  "90th_percentile_frame_rasterizer_time_millis": 7.481,
  "99th_percentile_frame_rasterizer_time_millis": 19.11,
  "worst_frame_rasterizer_time_millis": 79.799,
  "missed_frame_rasterizer_budget_count": 7,
  "frame_count": 234,
  "frame_rasterizer_count": 234,
  "new_gen_gc_count": 10,
  "old_gen_gc_count": 2,
 ```
 
 ### Impeller (ToT)
 
 ```
   "average_frame_build_time_millis": 1.431575000000001,
  "90th_percentile_frame_build_time_millis": 2.196,
  "99th_percentile_frame_build_time_millis": 14.486,
  "worst_frame_build_time_millis": 23.728,
  "missed_frame_build_budget_count": 2,
  "average_frame_rasterizer_time_millis": 6.536087499999999,
  "stddev_frame_rasterizer_time_millis": 1.9902712500000004,
  "90th_percentile_frame_rasterizer_time_millis": 9.705,
  "99th_percentile_frame_rasterizer_time_millis": 14.727,
  "worst_frame_rasterizer_time_millis": 17.838,
  "missed_frame_rasterizer_budget_count": 1,
  "frame_count": 240,
  "frame_rasterizer_count": 240,
  "new_gen_gc_count": 10,
  "old_gen_gc_count": 2,
  ```
  
  ### Impeller (Patched)
  
  ```
    "average_frame_build_time_millis": 1.4500167364016743,
  "90th_percentile_frame_build_time_millis": 2.478,
  "99th_percentile_frame_build_time_millis": 14.883,
  "worst_frame_build_time_millis": 18.782,
  "missed_frame_build_budget_count": 1,
  "average_frame_rasterizer_time_millis": 5.023033333333336,
  "stddev_frame_rasterizer_time_millis": 1.6445388888888894,
  "90th_percentile_frame_rasterizer_time_millis": 7.814,
  "99th_percentile_frame_rasterizer_time_millis": 13.497,
  "worst_frame_rasterizer_time_millis": 15.008,
  "missed_frame_rasterizer_budget_count": 0,
  "frame_count": 239,
  "frame_rasterizer_count": 240,
  "new_gen_gc_count": 8,
  "old_gen_gc_count": 0,
  ```
This commit is contained in:
Jonah Williams 2023-09-05 17:50:48 -07:00 committed by GitHub
parent 6aec1aa55e
commit 62031173dd
41 changed files with 411 additions and 125 deletions

View File

@ -87,6 +87,7 @@ source_set("display_list") {
public_deps = [
"//flutter/fml",
"//flutter/impeller/runtime_stage",
"//flutter/impeller/typographer",
"//third_party/skia",
]

View File

@ -637,6 +637,11 @@ void DisplayListGLComplexityCalculator::GLHelper::drawTextBlob(
draw_text_blob_count_++;
}
void DisplayListGLComplexityCalculator::GLHelper::drawTextFrame(
const std::shared_ptr<impeller::TextFrame>& text_frame,
SkScalar x,
SkScalar y) {}
void DisplayListGLComplexityCalculator::GLHelper::drawShadow(
const SkPath& path,
const DlColor color,

View File

@ -70,6 +70,9 @@ class DisplayListGLComplexityCalculator
void drawTextBlob(const sk_sp<SkTextBlob> blob,
SkScalar x,
SkScalar y) override;
void drawTextFrame(const std::shared_ptr<impeller::TextFrame>& text_frame,
SkScalar x,
SkScalar y) override;
void drawShadow(const SkPath& path,
const DlColor color,
const SkScalar elevation,

View File

@ -581,6 +581,11 @@ void DisplayListMetalComplexityCalculator::MetalHelper::drawTextBlob(
draw_text_blob_count_++;
}
void DisplayListMetalComplexityCalculator::MetalHelper::drawTextFrame(
const std::shared_ptr<impeller::TextFrame>& text_frame,
SkScalar x,
SkScalar y) {}
void DisplayListMetalComplexityCalculator::MetalHelper::drawShadow(
const SkPath& path,
const DlColor color,

View File

@ -70,6 +70,9 @@ class DisplayListMetalComplexityCalculator
void drawTextBlob(const sk_sp<SkTextBlob> blob,
SkScalar x,
SkScalar y) override;
void drawTextFrame(const std::shared_ptr<impeller::TextFrame>& text_frame,
SkScalar x,
SkScalar y) override;
void drawShadow(const SkPath& path,
const DlColor color,
const SkScalar elevation,

View File

@ -136,6 +136,7 @@ namespace flutter {
\
V(DrawDisplayList) \
V(DrawTextBlob) \
V(DrawTextFrame) \
\
V(DrawShadow) \
V(DrawShadowTransparentOccluder)

View File

@ -1302,6 +1302,48 @@ void DisplayListBuilder::DrawTextBlob(const sk_sp<SkTextBlob>& blob,
SetAttributesFromPaint(paint, DisplayListOpFlags::kDrawTextBlobFlags);
drawTextBlob(blob, x, y);
}
void DisplayListBuilder::drawTextFrame(
const std::shared_ptr<impeller::TextFrame>& text_frame,
SkScalar x,
SkScalar y) {
DisplayListAttributeFlags flags = kDrawTextBlobFlags;
OpResult result = PaintResult(current_, flags);
if (result == OpResult::kNoEffect) {
return;
}
impeller::Rect bounds = text_frame->GetBounds();
SkRect sk_bounds = SkRect::MakeLTRB(bounds.GetLeft(), bounds.GetTop(),
bounds.GetRight(), bounds.GetBottom());
bool unclipped = AccumulateOpBounds(sk_bounds.makeOffset(x, y), flags);
// TODO(https://github.com/flutter/flutter/issues/82202): Remove once the
// unit tests can use Fuchsia's font manager instead of the empty default.
// Until then we might encounter empty bounds for otherwise valid text and
// thus we ignore the results from AccumulateOpBounds.
#if defined(OS_FUCHSIA)
unclipped = true;
#endif // OS_FUCHSIA
if (unclipped) {
Push<DrawTextFrameOp>(0, 1, text_frame, x, y);
// There is no way to query if the glyphs of a text blob overlap and
// there are no current guarantees from either Skia or Impeller that
// they will protect overlapping glyphs from the effects of overdraw
// so we must make the conservative assessment that this DL layer is
// not compatible with group opacity inheritance.
UpdateLayerOpacityCompatibility(false);
UpdateLayerResult(result);
}
}
void DisplayListBuilder::DrawTextFrame(
const std::shared_ptr<impeller::TextFrame>& text_frame,
SkScalar x,
SkScalar y,
const DlPaint& paint) {
SetAttributesFromPaint(paint, DisplayListOpFlags::kDrawTextBlobFlags);
drawTextFrame(text_frame, x, y);
}
void DisplayListBuilder::DrawShadow(const SkPath& path,
const DlColor color,
const SkScalar elevation,

View File

@ -224,6 +224,16 @@ class DisplayListBuilder final : public virtual DlCanvas,
SkScalar x,
SkScalar y,
const DlPaint& paint) override;
void drawTextFrame(const std::shared_ptr<impeller::TextFrame>& text_frame,
SkScalar x,
SkScalar y) override;
void DrawTextFrame(const std::shared_ptr<impeller::TextFrame>& text_frame,
SkScalar x,
SkScalar y,
const DlPaint& paint) override;
// |DlCanvas|
void DrawShadow(const SkPath& path,
const DlColor color,

View File

@ -18,6 +18,8 @@
#include "third_party/skia/include/core/SkRect.h"
#include "third_party/skia/include/core/SkTextBlob.h"
#include "impeller/typographer/text_frame.h"
namespace flutter {
//------------------------------------------------------------------------------
@ -152,7 +154,7 @@ class DlCanvas {
virtual void DrawVertices(const DlVertices* vertices,
DlBlendMode mode,
const DlPaint& paint) = 0;
void DrawVertices(const std::shared_ptr<const DlVertices> vertices,
void DrawVertices(const std::shared_ptr<const DlVertices>& vertices,
DlBlendMode mode,
const DlPaint& paint) {
DrawVertices(vertices.get(), mode, paint);
@ -201,6 +203,13 @@ class DlCanvas {
const DlPaint* paint = nullptr) = 0;
virtual void DrawDisplayList(const sk_sp<DisplayList> display_list,
SkScalar opacity = SK_Scalar1) = 0;
virtual void DrawTextFrame(
const std::shared_ptr<impeller::TextFrame>& text_frame,
SkScalar x,
SkScalar y,
const DlPaint& paint) = 0;
virtual void DrawTextBlob(const sk_sp<SkTextBlob>& blob,
SkScalar x,
SkScalar y,

View File

@ -257,6 +257,10 @@ class DlOpReceiver {
virtual void drawTextBlob(const sk_sp<SkTextBlob> blob,
SkScalar x,
SkScalar y) = 0;
virtual void drawTextFrame(
const std::shared_ptr<impeller::TextFrame>& text_frame,
SkScalar x,
SkScalar y) = 0;
virtual void drawShadow(const SkPath& path,
const DlColor color,
const SkScalar elevation,

View File

@ -12,6 +12,7 @@
#include "flutter/display_list/effects/dl_color_source.h"
#include "flutter/fml/macros.h"
#include "impeller/typographer/text_frame.h"
#include "third_party/skia/include/core/SkRSXform.h"
namespace flutter {
@ -1084,6 +1085,25 @@ struct DrawTextBlobOp final : DrawOpBase {
}
};
struct DrawTextFrameOp final : DrawOpBase {
static const auto kType = DisplayListOpType::kDrawTextFrame;
DrawTextFrameOp(const std::shared_ptr<impeller::TextFrame>& text_frame,
SkScalar x,
SkScalar y)
: x(x), y(y), text_frame(text_frame) {}
const SkScalar x;
const SkScalar y;
const std::shared_ptr<impeller::TextFrame> text_frame;
void dispatch(DispatchContext& ctx) const {
if (op_needed(ctx)) {
ctx.receiver.drawTextFrame(text_frame, x, y);
}
}
};
// 4 byte header + 28 byte payload packs evenly into 32 bytes
#define DEFINE_DRAW_SHADOW_OP(name, transparent_occluder) \
struct Draw##name##Op final : DrawOpBase { \

View File

@ -325,6 +325,14 @@ void DlSkCanvasAdapter::DrawTextBlob(const sk_sp<SkTextBlob>& blob,
delegate_->drawTextBlob(blob, x, y, ToSk(paint));
}
void DlSkCanvasAdapter::DrawTextFrame(
const std::shared_ptr<impeller::TextFrame>& text_frame,
SkScalar x,
SkScalar y,
const DlPaint& paint) {
FML_CHECK(false);
}
void DlSkCanvasAdapter::DrawShadow(const SkPath& path,
const DlColor color,
const SkScalar elevation,

View File

@ -7,6 +7,7 @@
#include "flutter/display_list/dl_canvas.h"
#include "flutter/display_list/skia/dl_sk_types.h"
#include "impeller/typographer/text_frame.h"
namespace flutter {
@ -144,6 +145,10 @@ class DlSkCanvasAdapter final : public virtual DlCanvas {
SkScalar x,
SkScalar y,
const DlPaint& paint) override;
void DrawTextFrame(const std::shared_ptr<impeller::TextFrame>& text_frame,
SkScalar x,
SkScalar y,
const DlPaint& paint) override;
void DrawShadow(const SkPath& path,
const DlColor color,
const SkScalar elevation,

View File

@ -270,6 +270,13 @@ void DlSkCanvasDispatcher::drawTextBlob(const sk_sp<SkTextBlob> blob,
canvas_->drawTextBlob(blob, x, y, paint());
}
void DlSkCanvasDispatcher::drawTextFrame(
const std::shared_ptr<impeller::TextFrame>& text_frame,
SkScalar x,
SkScalar y) {
FML_CHECK(false);
}
void DlSkCanvasDispatcher::DrawShadow(SkCanvas* canvas,
const SkPath& path,
DlColor color,

View File

@ -98,6 +98,9 @@ class DlSkCanvasDispatcher : public virtual DlOpReceiver,
void drawTextBlob(const sk_sp<SkTextBlob> blob,
SkScalar x,
SkScalar y) override;
void drawTextFrame(const std::shared_ptr<impeller::TextFrame>& text_frame,
SkScalar x,
SkScalar y) override;
void drawShadow(const SkPath& path,
const DlColor color,
const SkScalar elevation,

View File

@ -129,6 +129,9 @@ class IgnoreDrawDispatchHelper : public virtual DlOpReceiver {
void drawTextBlob(const sk_sp<SkTextBlob> blob,
SkScalar x,
SkScalar y) override {}
void drawTextFrame(const std::shared_ptr<impeller::TextFrame>& text_frame,
SkScalar x,
SkScalar y) override {}
void drawShadow(const SkPath& path,
const DlColor color,
const SkScalar elevation,

View File

@ -99,7 +99,10 @@ source_set("flow") {
deps = [ "//third_party/skia" ]
if (impeller_supports_rendering) {
deps += [ "//flutter/impeller" ]
deps += [
"//flutter/impeller",
"//flutter/impeller/typographer/backends/skia:typographer_skia_backend",
]
}
}

View File

@ -14,6 +14,9 @@
#include "flow/stopwatch_sk.h"
#include "third_party/skia/include/core/SkFont.h"
#include "third_party/skia/include/core/SkTextBlob.h"
#ifdef IMPELLER_SUPPORTS_RENDERING
#include "impeller/typographer/backends/skia/text_frame_skia.h" // nogncheck
#endif // IMPELLER_SUPPORTS_RENDERING
namespace flutter {
namespace {
@ -50,7 +53,14 @@ void VisualizeStopWatch(DlCanvas* canvas,
stopwatch, label_prefix, font_path);
// Historically SK_ColorGRAY (== 0xFF888888) was used here
DlPaint paint(0xFF888888);
#ifdef IMPELLER_SUPPORTS_RENDERING
if (impeller_enabled) {
canvas->DrawTextFrame(impeller::MakeTextFrameFromTextBlobSkia(text),
x + label_x, y + height + label_y, paint);
}
#endif // IMPELLER_SUPPORTS_RENDERING
canvas->DrawTextBlob(text, x + label_x, y + height + label_y, paint);
return;
}
}

View File

@ -5,7 +5,6 @@
#include <array>
#include <cmath>
#include <cstdlib>
#include <iostream>
#include <memory>
#include <tuple>
#include <utility>
@ -1278,11 +1277,7 @@ bool RenderTextInCanvasSkia(const std::shared_ptr<Context>& context,
}
// Create the Impeller text frame and draw it at the designated baseline.
auto maybe_frame = MakeTextFrameFromTextBlobSkia(blob);
if (!maybe_frame.has_value()) {
return false;
}
auto frame = maybe_frame.value();
auto frame = MakeTextFrameFromTextBlobSkia(blob);
Paint text_paint;
text_paint.color = Color::Yellow().WithAlpha(options.alpha);
@ -1471,7 +1466,7 @@ TEST_P(AiksTest, CanRenderTextOutsideBoundaries) {
{
auto blob = SkTextBlob::MakeFromString(t.text, sk_font);
ASSERT_NE(blob, nullptr);
auto frame = MakeTextFrameFromTextBlobSkia(blob).value();
auto frame = MakeTextFrameFromTextBlobSkia(blob);
canvas.DrawTextFrame(frame, Point(), text_paint);
}
canvas.Restore();
@ -3094,12 +3089,7 @@ TEST_P(AiksTest, TextForegroundShaderWithTransform) {
auto blob = SkTextBlob::MakeFromString("Hello", sk_font);
ASSERT_NE(blob, nullptr);
auto maybe_frame = MakeTextFrameFromTextBlobSkia(blob);
ASSERT_TRUE(maybe_frame.has_value());
if (!maybe_frame.has_value()) {
return;
}
auto frame = maybe_frame.value();
auto frame = MakeTextFrameFromTextBlobSkia(blob);
canvas.DrawTextFrame(frame, Point(), text_paint);
ASSERT_TRUE(OpenPlaygroundHere(canvas.EndRecordingAsPicture()));

View File

@ -533,7 +533,7 @@ void Canvas::SaveLayer(const Paint& paint,
}
}
void Canvas::DrawTextFrame(const TextFrame& text_frame,
void Canvas::DrawTextFrame(const std::shared_ptr<TextFrame>& text_frame,
Point position,
const Paint& paint) {
Entity entity;
@ -541,7 +541,7 @@ void Canvas::DrawTextFrame(const TextFrame& text_frame,
entity.SetBlendMode(paint.blend_mode);
auto text_contents = std::make_shared<TextContents>();
text_contents->SetTextFrame(TextFrame(text_frame));
text_contents->SetTextFrame(text_frame);
text_contents->SetColor(paint.color);
entity.SetTransformation(GetCurrentTransformation() *

View File

@ -139,7 +139,7 @@ class Canvas {
void DrawPicture(const Picture& picture);
void DrawTextFrame(const TextFrame& text_frame,
void DrawTextFrame(const std::shared_ptr<TextFrame>& text_frame,
Point position,
const Paint& paint);

View File

@ -34,7 +34,6 @@
#include "impeller/geometry/path_builder.h"
#include "impeller/geometry/scalar.h"
#include "impeller/geometry/sigma.h"
#include "impeller/typographer/backends/skia/text_frame_skia.h"
#if IMPELLER_ENABLE_3D
#include "impeller/entity/contents/scene_contents.h"
@ -1110,20 +1109,14 @@ void DlDispatcher::drawDisplayList(
void DlDispatcher::drawTextBlob(const sk_sp<SkTextBlob> blob,
SkScalar x,
SkScalar y) {
const auto maybe_text_frame = MakeTextFrameFromTextBlobSkia(blob);
if (!maybe_text_frame.has_value()) {
return;
}
const auto text_frame = maybe_text_frame.value();
if (paint_.style == Paint::Style::kStroke ||
paint_.color_source.GetType() != ColorSource::Type::kColor) {
auto bounds = blob->bounds();
auto path = skia_conversions::PathDataFromTextBlob(
blob, Point(x + bounds.left(), y + bounds.top()));
canvas_.DrawPath(path, paint_);
return;
}
// When running with Impeller enabled Skia text blobs are converted to
// Impeller text frames in paragraph_skia.cc
UNIMPLEMENTED;
}
void DlDispatcher::drawTextFrame(const std::shared_ptr<TextFrame>& text_frame,
SkScalar x,
SkScalar y) {
canvas_.DrawTextFrame(text_frame, //
impeller::Point{x, y}, //
paint_ //

View File

@ -212,6 +212,11 @@ class DlDispatcher final : public flutter::DlOpReceiver {
SkScalar x,
SkScalar y) override;
// |flutter::DlOpReceiver|
void drawTextFrame(const std::shared_ptr<impeller::TextFrame>& text_frame,
SkScalar x,
SkScalar y) override;
// |flutter::DlOpReceiver|
void drawShadow(const SkPath& path,
const flutter::DlColor color,

View File

@ -24,8 +24,8 @@ TextContents::TextContents() = default;
TextContents::~TextContents() = default;
void TextContents::SetTextFrame(TextFrame&& frame) {
frame_ = std::move(frame);
void TextContents::SetTextFrame(const std::shared_ptr<TextFrame>& frame) {
frame_ = frame;
}
std::shared_ptr<GlyphAtlas> TextContents::ResolveAtlas(
@ -49,7 +49,7 @@ Color TextContents::GetColor() const {
}
bool TextContents::CanInheritOpacity(const Entity& entity) const {
return !frame_.MaybeHasOverlapping();
return !frame_->MaybeHasOverlapping();
}
void TextContents::SetInheritedOpacity(Scalar opacity) {
@ -61,13 +61,13 @@ void TextContents::SetOffset(Vector2 offset) {
}
std::optional<Rect> TextContents::GetCoverage(const Entity& entity) const {
return frame_.GetBounds().TransformBounds(entity.GetTransformation());
return frame_->GetBounds().TransformBounds(entity.GetTransformation());
}
void TextContents::PopulateGlyphAtlas(
const std::shared_ptr<LazyGlyphAtlas>& lazy_glyph_atlas,
Scalar scale) {
lazy_glyph_atlas->AddTextFrame(frame_, scale);
lazy_glyph_atlas->AddTextFrame(*frame_, scale);
scale_ = scale;
}
@ -79,7 +79,7 @@ bool TextContents::Render(const ContentContext& renderer,
return true;
}
auto type = frame_.GetAtlasType();
auto type = frame_->GetAtlasType();
auto atlas =
ResolveAtlas(*renderer.GetContext(), type, renderer.GetLazyGlyphAtlas());
@ -152,7 +152,7 @@ bool TextContents::Render(const ContentContext& renderer,
auto& host_buffer = pass.GetTransientsBuffer();
size_t vertex_count = 0;
for (const auto& run : frame_.GetRuns()) {
for (const auto& run : frame_->GetRuns()) {
vertex_count += run.GetGlyphPositions().size();
}
vertex_count *= 6;
@ -163,7 +163,7 @@ bool TextContents::Render(const ContentContext& renderer,
VS::PerVertexData vtx;
VS::PerVertexData* vtx_contents =
reinterpret_cast<VS::PerVertexData*>(contents);
for (const TextRun& run : frame_.GetRuns()) {
for (const TextRun& run : frame_->GetRuns()) {
const Font& font = run.GetFont();
Scalar rounded_scale = TextFrame::RoundScaledFontSize(
scale_, font.GetMetrics().point_size);

View File

@ -26,7 +26,7 @@ class TextContents final : public Contents {
~TextContents();
void SetTextFrame(TextFrame&& frame);
void SetTextFrame(const std::shared_ptr<TextFrame>& frame);
void SetColor(Color color);
@ -56,7 +56,7 @@ class TextContents final : public Contents {
RenderPass& pass) const override;
private:
TextFrame frame_;
std::shared_ptr<TextFrame> frame_;
Scalar scale_ = 1.0;
Color color_;
Scalar inherited_opacity_ = 1.0;

View File

@ -2174,13 +2174,13 @@ TEST_P(EntityTest, InheritOpacityTest) {
SkFont font;
font.setSize(30);
auto blob = SkTextBlob::MakeFromString("A", font);
auto frame = MakeTextFrameFromTextBlobSkia(blob).value();
auto frame = MakeTextFrameFromTextBlobSkia(blob);
auto lazy_glyph_atlas =
std::make_shared<LazyGlyphAtlas>(TypographerContextSkia::Make());
lazy_glyph_atlas->AddTextFrame(frame, 1.0f);
lazy_glyph_atlas->AddTextFrame(*frame, 1.0f);
auto text_contents = std::make_shared<TextContents>();
text_contents->SetTextFrame(std::move(frame));
text_contents->SetTextFrame(frame);
text_contents->SetColor(Color::Blue().WithAlpha(0.5));
ASSERT_TRUE(text_contents->CanInheritOpacity(entity));

View File

@ -8,7 +8,6 @@
#include "flutter/fml/logging.h"
#include "impeller/typographer/backends/skia/typeface_skia.h"
#include "include/core/SkFontTypes.h"
#include "include/core/SkRect.h"
#include "third_party/skia/include/core/SkFont.h"
#include "third_party/skia/include/core/SkFontMetrics.h"
@ -39,16 +38,10 @@ static Rect ToRect(const SkRect& rect) {
static constexpr Scalar kScaleSize = 100000.0f;
std::optional<TextFrame> MakeTextFrameFromTextBlobSkia(
std::shared_ptr<TextFrame> MakeTextFrameFromTextBlobSkia(
const sk_sp<SkTextBlob>& blob) {
// Handling nullptr text blobs feels overly defensive here, as I don't
// actually know if this happens.
if (!blob) {
return {};
}
std::vector<TextRun> runs;
bool has_color = false;
std::vector<TextRun> runs;
for (SkTextBlobRunIterator run(blob.get()); !run.done(); run.next()) {
// TODO(jonahwilliams): ask Skia for a public API to look this up.
// https://github.com/flutter/flutter/issues/112005
@ -95,7 +88,7 @@ std::optional<TextFrame> MakeTextFrameFromTextBlobSkia(
continue;
}
}
return TextFrame(runs, ToRect(blob->bounds()), has_color);
return std::make_shared<TextFrame>(runs, ToRect(blob->bounds()), has_color);
}
} // namespace impeller

View File

@ -4,13 +4,12 @@
#pragma once
#include "flutter/fml/macros.h"
#include "impeller/typographer/text_frame.h"
#include "third_party/skia/include/core/SkTextBlob.h"
namespace impeller {
std::optional<TextFrame> MakeTextFrameFromTextBlobSkia(
std::shared_ptr<impeller::TextFrame> MakeTextFrameFromTextBlobSkia(
const sk_sp<SkTextBlob>& blob);
} // namespace impeller

View File

@ -8,9 +8,10 @@
namespace impeller {
TextFrame MakeTextFrameSTB(const std::shared_ptr<TypefaceSTB>& typeface_stb,
Font::Metrics metrics,
const std::string& text) {
std::shared_ptr<TextFrame> MakeTextFrameSTB(
const std::shared_ptr<TypefaceSTB>& typeface_stb,
Font::Metrics metrics,
const std::string& text) {
TextRun run(Font(typeface_stb, metrics));
// Shape the text run using STB. The glyph positions could also be resolved
@ -61,7 +62,8 @@ TextFrame MakeTextFrameSTB(const std::shared_ptr<TypefaceSTB>& typeface_stb,
}
std::vector<TextRun> runs = {run};
return TextFrame(runs, result.value_or(Rect::MakeLTRB(0, 0, 0, 0)), false);
return std::make_shared<TextFrame>(
runs, result.value_or(Rect::MakeLTRB(0, 0, 0, 0)), false);
}
} // namespace impeller

View File

@ -10,8 +10,9 @@
namespace impeller {
TextFrame MakeTextFrameSTB(const std::shared_ptr<TypefaceSTB>& typeface_stb,
Font::Metrics metrics,
const std::string& text);
std::shared_ptr<TextFrame> MakeTextFrameSTB(
const std::shared_ptr<TypefaceSTB>& typeface_stb,
Font::Metrics metrics,
const std::string& text);
} // namespace impeller

View File

@ -4,7 +4,6 @@
#pragma once
#include <functional>
#include <memory>
#include "flutter/fml/macros.h"

View File

@ -40,9 +40,9 @@ TEST_P(TypographerTest, CanConvertTextBlob) {
auto blob = SkTextBlob::MakeFromString(
"the quick brown fox jumped over the lazy dog.", font);
ASSERT_TRUE(blob);
auto frame = MakeTextFrameFromTextBlobSkia(blob).value();
ASSERT_EQ(frame.GetRunCount(), 1u);
for (const auto& run : frame.GetRuns()) {
auto frame = MakeTextFrameFromTextBlobSkia(blob);
ASSERT_EQ(frame->GetRunCount(), 1u);
for (const auto& run : frame->GetRuns()) {
ASSERT_TRUE(run.IsValid());
ASSERT_EQ(run.GetGlyphCount(), 45u);
}
@ -62,7 +62,7 @@ TEST_P(TypographerTest, CanCreateGlyphAtlas) {
ASSERT_TRUE(blob);
auto atlas = CreateGlyphAtlas(
*GetContext(), context.get(), GlyphAtlas::Type::kAlphaBitmap, 1.0f,
atlas_context, MakeTextFrameFromTextBlobSkia(blob).value());
atlas_context, *MakeTextFrameFromTextBlobSkia(blob));
ASSERT_NE(atlas, nullptr);
ASSERT_NE(atlas->GetTexture(), nullptr);
ASSERT_EQ(atlas->GetType(), GlyphAtlas::Type::kAlphaBitmap);
@ -113,21 +113,20 @@ TEST_P(TypographerTest, LazyAtlasTracksColor) {
auto blob = SkTextBlob::MakeFromString("hello", sk_font);
ASSERT_TRUE(blob);
auto frame = MakeTextFrameFromTextBlobSkia(blob).value();
auto frame = MakeTextFrameFromTextBlobSkia(blob);
ASSERT_FALSE(frame.GetAtlasType() == GlyphAtlas::Type::kColorBitmap);
ASSERT_FALSE(frame->GetAtlasType() == GlyphAtlas::Type::kColorBitmap);
LazyGlyphAtlas lazy_atlas(TypographerContextSkia::Make());
lazy_atlas.AddTextFrame(frame, 1.0f);
lazy_atlas.AddTextFrame(*frame, 1.0f);
frame = MakeTextFrameFromTextBlobSkia(
SkTextBlob::MakeFromString("😀 ", emoji_font))
.value();
SkTextBlob::MakeFromString("😀 ", emoji_font));
ASSERT_TRUE(frame.GetAtlasType() == GlyphAtlas::Type::kColorBitmap);
ASSERT_TRUE(frame->GetAtlasType() == GlyphAtlas::Type::kColorBitmap);
lazy_atlas.AddTextFrame(frame, 1.0f);
lazy_atlas.AddTextFrame(*frame, 1.0f);
// Creates different atlases for color and alpha bitmap.
auto color_atlas = lazy_atlas.CreateOrGetGlyphAtlas(
@ -148,7 +147,7 @@ TEST_P(TypographerTest, GlyphAtlasWithOddUniqueGlyphSize) {
ASSERT_TRUE(blob);
auto atlas = CreateGlyphAtlas(
*GetContext(), context.get(), GlyphAtlas::Type::kAlphaBitmap, 1.0f,
atlas_context, MakeTextFrameFromTextBlobSkia(blob).value());
atlas_context, *MakeTextFrameFromTextBlobSkia(blob));
ASSERT_NE(atlas, nullptr);
ASSERT_NE(atlas->GetTexture(), nullptr);
@ -165,7 +164,7 @@ TEST_P(TypographerTest, GlyphAtlasIsRecycledIfUnchanged) {
ASSERT_TRUE(blob);
auto atlas = CreateGlyphAtlas(
*GetContext(), context.get(), GlyphAtlas::Type::kAlphaBitmap, 1.0f,
atlas_context, MakeTextFrameFromTextBlobSkia(blob).value());
atlas_context, *MakeTextFrameFromTextBlobSkia(blob));
ASSERT_NE(atlas, nullptr);
ASSERT_NE(atlas->GetTexture(), nullptr);
ASSERT_EQ(atlas, atlas_context->GetGlyphAtlas());
@ -174,7 +173,7 @@ TEST_P(TypographerTest, GlyphAtlasIsRecycledIfUnchanged) {
auto next_atlas = CreateGlyphAtlas(
*GetContext(), context.get(), GlyphAtlas::Type::kAlphaBitmap, 1.0f,
atlas_context, MakeTextFrameFromTextBlobSkia(blob).value());
atlas_context, *MakeTextFrameFromTextBlobSkia(blob));
ASSERT_EQ(atlas, next_atlas);
ASSERT_EQ(atlas_context->GetGlyphAtlas(), atlas);
}
@ -197,7 +196,7 @@ TEST_P(TypographerTest, GlyphAtlasWithLotsOfdUniqueGlyphSize) {
FontGlyphMap font_glyph_map;
size_t size_count = 8;
for (size_t index = 0; index < size_count; index += 1) {
MakeTextFrameFromTextBlobSkia(blob).value().CollectUniqueFontGlyphPairs(
MakeTextFrameFromTextBlobSkia(blob)->CollectUniqueFontGlyphPairs(
font_glyph_map, 0.6 * index);
};
auto atlas =
@ -232,7 +231,7 @@ TEST_P(TypographerTest, GlyphAtlasTextureIsRecycledIfUnchanged) {
ASSERT_TRUE(blob);
auto atlas = CreateGlyphAtlas(
*GetContext(), context.get(), GlyphAtlas::Type::kAlphaBitmap, 1.0f,
atlas_context, MakeTextFrameFromTextBlobSkia(blob).value());
atlas_context, *MakeTextFrameFromTextBlobSkia(blob));
auto old_packer = atlas_context->GetRectPacker();
ASSERT_NE(atlas, nullptr);
@ -246,7 +245,7 @@ TEST_P(TypographerTest, GlyphAtlasTextureIsRecycledIfUnchanged) {
auto blob2 = SkTextBlob::MakeFromString("spooky 2", sk_font);
auto next_atlas = CreateGlyphAtlas(
*GetContext(), context.get(), GlyphAtlas::Type::kAlphaBitmap, 1.0f,
atlas_context, MakeTextFrameFromTextBlobSkia(blob2).value());
atlas_context, *MakeTextFrameFromTextBlobSkia(blob2));
ASSERT_EQ(atlas, next_atlas);
auto* second_texture = next_atlas->GetTexture().get();
@ -265,7 +264,7 @@ TEST_P(TypographerTest, GlyphAtlasTextureIsRecreatedIfTypeChanges) {
ASSERT_TRUE(blob);
auto atlas = CreateGlyphAtlas(
*GetContext(), context.get(), GlyphAtlas::Type::kAlphaBitmap, 1.0f,
atlas_context, MakeTextFrameFromTextBlobSkia(blob).value());
atlas_context, *MakeTextFrameFromTextBlobSkia(blob));
auto old_packer = atlas_context->GetRectPacker();
ASSERT_NE(atlas, nullptr);
@ -280,7 +279,7 @@ TEST_P(TypographerTest, GlyphAtlasTextureIsRecreatedIfTypeChanges) {
auto blob2 = SkTextBlob::MakeFromString("spooky 1", sk_font);
auto next_atlas = CreateGlyphAtlas(
*GetContext(), context.get(), GlyphAtlas::Type::kColorBitmap, 1.0f,
atlas_context, MakeTextFrameFromTextBlobSkia(blob2).value());
atlas_context, *MakeTextFrameFromTextBlobSkia(blob2));
ASSERT_NE(atlas, next_atlas);
auto* second_texture = next_atlas->GetTexture().get();
@ -297,15 +296,13 @@ TEST_P(TypographerTest, MaybeHasOverlapping) {
SkFont sk_font(typeface, 0.5f);
auto frame =
MakeTextFrameFromTextBlobSkia(SkTextBlob::MakeFromString("1", sk_font))
.value();
MakeTextFrameFromTextBlobSkia(SkTextBlob::MakeFromString("1", sk_font));
// Single character has no overlapping
ASSERT_FALSE(frame.MaybeHasOverlapping());
ASSERT_FALSE(frame->MaybeHasOverlapping());
auto frame_2 = MakeTextFrameFromTextBlobSkia(
SkTextBlob::MakeFromString("123456789", sk_font))
.value();
ASSERT_FALSE(frame_2.MaybeHasOverlapping());
SkTextBlob::MakeFromString("123456789", sk_font));
ASSERT_FALSE(frame_2->MaybeHasOverlapping());
}
TEST_P(TypographerTest, RectanglePackerAddsNonoverlapingRectangles) {

View File

@ -127,6 +127,14 @@ void DlOpSpy::drawTextBlob(const sk_sp<SkTextBlob> blob,
SkScalar y) {
did_draw_ |= will_draw_;
}
void DlOpSpy::drawTextFrame(
const std::shared_ptr<impeller::TextFrame>& text_frame,
SkScalar x,
SkScalar y) {
did_draw_ |= will_draw_;
}
void DlOpSpy::drawShadow(const SkPath& path,
const DlColor color,
const SkScalar elevation,

View File

@ -90,6 +90,9 @@ class DlOpSpy final : public virtual DlOpReceiver,
void drawTextBlob(const sk_sp<SkTextBlob> blob,
SkScalar x,
SkScalar y) override;
void drawTextFrame(const std::shared_ptr<impeller::TextFrame>& text_frame,
SkScalar x,
SkScalar y) override;
void drawShadow(const SkPath& path,
const DlColor color,
const SkScalar elevation,

View File

@ -859,6 +859,15 @@ void DisplayListStreamDispatcher::drawTextBlob(const sk_sp<SkTextBlob> blob,
<< blob.get() << ", "
<< x << ", " << y << ");" << std::endl;
}
void DisplayListStreamDispatcher::drawTextFrame(const std::shared_ptr<impeller::TextFrame>& text_frame,
SkScalar x,
SkScalar y) {
startl() << "drawTextFrame("
<< text_frame.get() << ", "
<< x << ", " << y << ");" << std::endl;
}
void DisplayListStreamDispatcher::drawShadow(const SkPath& path,
const DlColor color,
const SkScalar elevation,

View File

@ -144,6 +144,9 @@ class DisplayListStreamDispatcher final : public DlOpReceiver {
void drawTextBlob(const sk_sp<SkTextBlob> blob,
SkScalar x,
SkScalar y) override;
void drawTextFrame(const std::shared_ptr<impeller::TextFrame>& text_frame,
SkScalar x,
SkScalar y) override;
void drawShadow(const SkPath& path,
const DlColor color,
const SkScalar elevation,

View File

@ -160,6 +160,14 @@ void MockCanvas::DrawTextBlob(const sk_sp<SkTextBlob>& text,
paint, SkPoint::Make(x, y)}});
}
void MockCanvas::DrawTextFrame(
const std::shared_ptr<impeller::TextFrame>& text_frame,
SkScalar x,
SkScalar y,
const DlPaint& paint) {
FML_DCHECK(false);
}
void MockCanvas::DrawRect(const SkRect& rect, const DlPaint& paint) {
draw_calls_.emplace_back(DrawCall{current_layer_, DrawRectData{rect, paint}});
}

View File

@ -273,6 +273,10 @@ class MockCanvas final : public DlCanvas {
SkScalar x,
SkScalar y,
const DlPaint& paint) override;
void DrawTextFrame(const std::shared_ptr<impeller::TextFrame>& text_frame,
SkScalar x,
SkScalar y,
const DlPaint& paint) override;
void DrawShadow(const SkPath& path,
const DlColor color,
const SkScalar elevation,

View File

@ -81,6 +81,7 @@ source_set("txt") {
public_deps = [
"//flutter/display_list",
"//flutter/fml",
"//flutter/impeller/typographer/backends/skia:typographer_skia_backend",
"//third_party/harfbuzz",
"//third_party/icu",
"//third_party/skia",

View File

@ -18,7 +18,10 @@
#include <algorithm>
#include <numeric>
#include "display_list/dl_paint.h"
#include "fml/logging.h"
#include "impeller/typographer/backends/skia/text_frame_skia.h"
#include "include/core/SkMatrix.h"
namespace txt {
@ -65,10 +68,10 @@ class DisplayListParagraphPainter : public skt::ParagraphPainter {
/// decision (i.e. with `#ifdef`) instead of a runtime option.
DisplayListParagraphPainter(DisplayListBuilder* builder,
const std::vector<DlPaint>& dl_paints,
bool draw_path_effect)
bool impeller_enabled)
: builder_(builder),
dl_paints_(dl_paints),
draw_path_effect_(draw_path_effect) {}
impeller_enabled_(impeller_enabled) {}
void drawTextBlob(const sk_sp<SkTextBlob>& blob,
SkScalar x,
@ -79,6 +82,22 @@ class DisplayListParagraphPainter : public skt::ParagraphPainter {
}
size_t paint_id = std::get<PaintID>(paint);
FML_DCHECK(paint_id < dl_paints_.size());
#ifdef IMPELLER_SUPPORTS_RENDERING
if (impeller_enabled_) {
if (ShouldRenderAsPath(dl_paints_[paint_id])) {
auto path = skia::textlayout::Paragraph::GetPath(blob.get());
auto transformed = path.makeTransform(SkMatrix::Translate(
x + blob->bounds().left(), y + blob->bounds().top()));
builder_->DrawPath(transformed, dl_paints_[paint_id]);
return;
}
builder_->DrawTextFrame(impeller::MakeTextFrameFromTextBlobSkia(blob), x,
y, dl_paints_[paint_id]);
return;
}
#endif // IMPELLER_SUPPORTS_RENDERING
builder_->DrawTextBlob(blob, x, y, dl_paints_[paint_id]);
}
@ -96,6 +115,11 @@ class DisplayListParagraphPainter : public skt::ParagraphPainter {
DlBlurMaskFilter filter(DlBlurStyle::kNormal, blur_sigma, false);
paint.setMaskFilter(&filter);
}
if (impeller_enabled_) {
builder_->DrawTextFrame(impeller::MakeTextFrameFromTextBlobSkia(blob), x,
y, paint);
return;
}
builder_->DrawTextBlob(blob, x, y, paint);
}
@ -129,11 +153,13 @@ class DisplayListParagraphPainter : public skt::ParagraphPainter {
// the line directly using the `drawLine` API instead of using a path effect
// (because Impeller does not support path effects).
auto dash_path_effect = decor_style.getDashPathEffect();
if (draw_path_effect_ && dash_path_effect) {
#ifdef IMPELLER_SUPPORTS_RENDERING
if (impeller_enabled_ && dash_path_effect) {
auto path = dashedLine(x0, x1, y0, *dash_path_effect);
builder_->DrawPath(path, toDlPaint(decor_style));
return;
}
#endif // IMPELLER_SUPPORTS_RENDERING
auto paint = toDlPaint(decor_style);
if (dash_path_effect) {
@ -180,6 +206,16 @@ class DisplayListParagraphPainter : public skt::ParagraphPainter {
return path;
}
bool ShouldRenderAsPath(const DlPaint& paint) const {
FML_DCHECK(impeller_enabled_);
// Text with non-trivial color sources or stroke paint mode should be
// rendered as a path when running on Impeller for correctness. These
// filters rely on having the glyph coverage, whereas regular text is
// drawn as rectangular texture samples.
return ((paint.getColorSource() && !paint.getColorSource()->asColor()) ||
paint.getDrawStyle() == DlDrawStyle::kStroke);
}
DlPaint toDlPaint(const DecorationStyle& decor_style,
DlDrawStyle draw_style = DlDrawStyle::kStroke) {
DlPaint paint;
@ -192,7 +228,7 @@ class DisplayListParagraphPainter : public skt::ParagraphPainter {
void setPathEffect(DlPaint& paint, const DashPathEffect& dash_path_effect) {
// Impeller does not support path effects, so we should never be setting.
FML_DCHECK(!draw_path_effect_);
FML_DCHECK(!impeller_enabled_);
std::array<SkScalar, 2> intervals{dash_path_effect.fOnLength,
dash_path_effect.fOffLength};
@ -202,7 +238,7 @@ class DisplayListParagraphPainter : public skt::ParagraphPainter {
DisplayListBuilder* builder_;
const std::vector<DlPaint>& dl_paints_;
bool draw_path_effect_;
const bool impeller_enabled_;
};
} // anonymous namespace

View File

@ -3,8 +3,13 @@
// found in the LICENSE file.
#include <memory>
#include "display_list/dl_color.h"
#include "display_list/dl_paint.h"
#include "display_list/dl_tile_mode.h"
#include "display_list/effects/dl_color_source.h"
#include "display_list/utils/dl_receiver_utils.h"
#include "gtest/gtest.h"
#include "include/core/SkScalar.h"
#include "runtime/test_font_data.h"
#include "skia/paragraph_builder_skia.h"
#include "testing/canvas_test.h"
@ -23,6 +28,8 @@ class DlOpRecorder final : public virtual DlOpReceiver,
int lineCount() const { return lines_.size(); }
int rectCount() const { return rects_.size(); }
int pathCount() const { return paths_.size(); }
int textFrameCount() const { return text_frames_.size(); }
int blobCount() const { return blobs_.size(); }
bool hasPathEffect() const { return path_effect_ != nullptr; }
private:
@ -30,6 +37,18 @@ class DlOpRecorder final : public virtual DlOpReceiver,
lines_.emplace_back(p0, p1);
}
void drawTextFrame(const std::shared_ptr<impeller::TextFrame>& text_frame,
SkScalar x,
SkScalar y) override {
text_frames_.push_back(text_frame);
}
void drawTextBlob(const sk_sp<SkTextBlob> blob,
SkScalar x,
SkScalar y) override {
blobs_.push_back(blob);
}
void drawRect(const SkRect& rect) override { rects_.push_back(rect); }
void drawPath(const SkPath& path) override { paths_.push_back(path); }
@ -38,6 +57,8 @@ class DlOpRecorder final : public virtual DlOpReceiver,
path_effect_ = effect;
}
std::vector<std::shared_ptr<impeller::TextFrame>> text_frames_;
std::vector<sk_sp<SkTextBlob>> blobs_;
std::vector<std::pair<SkPoint, SkPoint>> lines_;
std::vector<SkRect> rects_;
std::vector<SkPath> paths_;
@ -52,10 +73,30 @@ class PainterTestBase : public CanvasTestBase<T> {
void PretendImpellerIsEnabled(bool impeller) { impeller_ = impeller; }
protected:
sk_sp<DisplayList> draw(txt::TextDecorationStyle style) const {
auto t_style = makeDecoratedStyle(style);
txt::TextStyle makeDecoratedStyle(txt::TextDecorationStyle style) {
auto t_style = txt::TextStyle();
t_style.color = SK_ColorBLACK; // default
t_style.font_weight = txt::FontWeight::w400; // normal
t_style.font_size = 14; // default
t_style.decoration = txt::TextDecoration::kUnderline;
t_style.decoration_style = style;
t_style.decoration_color = SK_ColorBLACK;
t_style.font_families.push_back("ahem");
return t_style;
}
txt::TextStyle makeStyle() {
auto t_style = txt::TextStyle();
t_style.color = SK_ColorBLACK; // default
t_style.font_weight = txt::FontWeight::w400; // normal
t_style.font_size = 14; // default
t_style.font_families.push_back("ahem");
return t_style;
}
sk_sp<DisplayList> draw(txt::TextStyle style) const {
auto pb_skia = makeParagraphBuilder();
pb_skia.PushStyle(t_style);
pb_skia.PushStyle(style);
pb_skia.AddText(u"Hello World!");
pb_skia.Pop();
@ -85,18 +126,6 @@ class PainterTestBase : public CanvasTestBase<T> {
return txt::ParagraphBuilderSkia(p_style, f_collection, impeller_);
}
txt::TextStyle makeDecoratedStyle(txt::TextDecorationStyle style) const {
auto t_style = txt::TextStyle();
t_style.color = SK_ColorBLACK; // default
t_style.font_weight = txt::FontWeight::w400; // normal
t_style.font_size = 14; // default
t_style.decoration = txt::TextDecoration::kUnderline;
t_style.decoration_style = style;
t_style.decoration_color = SK_ColorBLACK;
t_style.font_families.push_back("ahem");
return t_style;
}
bool impeller_ = false;
};
@ -106,19 +135,8 @@ TEST_F(PainterTest, DrawsSolidLineSkia) {
PretendImpellerIsEnabled(false);
auto recorder = DlOpRecorder();
draw(txt::TextDecorationStyle::kSolid)->Dispatch(recorder);
// Skia may draw a solid underline as a filled rectangle:
// https://skia.googlesource.com/skia/+/refs/heads/main/modules/skparagraph/src/Decorations.cpp#91
EXPECT_EQ(recorder.rectCount(), 1);
EXPECT_FALSE(recorder.hasPathEffect());
}
TEST_F(PainterTest, DrawsSolidLineImpeller) {
PretendImpellerIsEnabled(true);
auto recorder = DlOpRecorder();
draw(txt::TextDecorationStyle::kSolid)->Dispatch(recorder);
draw(makeDecoratedStyle(txt::TextDecorationStyle::kSolid))
->Dispatch(recorder);
// Skia may draw a solid underline as a filled rectangle:
// https://skia.googlesource.com/skia/+/refs/heads/main/modules/skparagraph/src/Decorations.cpp#91
@ -130,23 +148,98 @@ TEST_F(PainterTest, DrawDashedLineSkia) {
PretendImpellerIsEnabled(false);
auto recorder = DlOpRecorder();
draw(txt::TextDecorationStyle::kDashed)->Dispatch(recorder);
draw(makeDecoratedStyle(txt::TextDecorationStyle::kDashed))
->Dispatch(recorder);
// Skia draws a dashed underline as a filled rectangle with a path effect.
EXPECT_EQ(recorder.lineCount(), 1);
EXPECT_TRUE(recorder.hasPathEffect());
}
#ifdef IMPELLER_SUPPORTS_RENDERING
TEST_F(PainterTest, DrawsSolidLineImpeller) {
PretendImpellerIsEnabled(true);
auto recorder = DlOpRecorder();
draw(makeDecoratedStyle(txt::TextDecorationStyle::kSolid))
->Dispatch(recorder);
// Skia may draw a solid underline as a filled rectangle:
// https://skia.googlesource.com/skia/+/refs/heads/main/modules/skparagraph/src/Decorations.cpp#91
EXPECT_EQ(recorder.rectCount(), 1);
EXPECT_FALSE(recorder.hasPathEffect());
}
TEST_F(PainterTest, DrawDashedLineImpeller) {
PretendImpellerIsEnabled(true);
auto recorder = DlOpRecorder();
draw(txt::TextDecorationStyle::kDashed)->Dispatch(recorder);
draw(makeDecoratedStyle(txt::TextDecorationStyle::kDashed))
->Dispatch(recorder);
// Impeller draws a dashed underline as a path.
EXPECT_EQ(recorder.pathCount(), 1);
EXPECT_FALSE(recorder.hasPathEffect());
}
TEST_F(PainterTest, DrawTextFrameImpeller) {
PretendImpellerIsEnabled(true);
auto recorder = DlOpRecorder();
draw(makeStyle())->Dispatch(recorder);
EXPECT_EQ(recorder.textFrameCount(), 1);
EXPECT_EQ(recorder.blobCount(), 0);
}
TEST_F(PainterTest, DrawStrokedTextImpeller) {
PretendImpellerIsEnabled(true);
auto style = makeStyle();
// What is your shtyle?
DlPaint foreground;
foreground.setDrawStyle(DlDrawStyle::kStroke);
style.foreground = foreground;
auto recorder = DlOpRecorder();
draw(style)->Dispatch(recorder);
EXPECT_EQ(recorder.textFrameCount(), 0);
EXPECT_EQ(recorder.blobCount(), 0);
EXPECT_EQ(recorder.pathCount(), 1);
}
TEST_F(PainterTest, DrawTextWithGradientImpeller) {
PretendImpellerIsEnabled(true);
auto style = makeStyle();
// how do you like my shtyle?
DlPaint foreground;
std::vector<DlColor> colors = {DlColor::kRed(), DlColor::kCyan()};
std::vector<float> stops = {0.0, 1.0};
foreground.setColorSource(DlColorSource::MakeLinear(
SkPoint::Make(0, 0), SkPoint::Make(100, 100), 2, colors.data(),
stops.data(), DlTileMode::kClamp));
style.foreground = foreground;
auto recorder = DlOpRecorder();
draw(style)->Dispatch(recorder);
EXPECT_EQ(recorder.textFrameCount(), 0);
EXPECT_EQ(recorder.blobCount(), 0);
EXPECT_EQ(recorder.pathCount(), 1);
}
TEST_F(PainterTest, DrawTextBlobNoImpeller) {
PretendImpellerIsEnabled(false);
auto recorder = DlOpRecorder();
draw(makeStyle())->Dispatch(recorder);
EXPECT_EQ(recorder.textFrameCount(), 0);
EXPECT_EQ(recorder.blobCount(), 1);
}
#endif // IMPELLER_SUPPORTS_RENDERING
} // namespace testing
} // namespace flutter