blob: c492269101208ab189b6ba7f55bee358e0ef8d62 [file] [log] [blame]
/*
* Copyright 2017 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#ifndef LIB_TXT_SRC_PARAGRAPH_H_
#define LIB_TXT_SRC_PARAGRAPH_H_
#include <set>
#include <utility>
#include <vector>
#include "flutter/fml/compiler_specific.h"
#include "flutter/fml/macros.h"
#include "font_collection.h"
#include "minikin/LineBreaker.h"
#include "paint_record.h"
#include "paragraph_style.h"
#include "placeholder_run.h"
#include "styled_runs.h"
#include "third_party/googletest/googletest/include/gtest/gtest_prod.h" // nogncheck
#include "third_party/skia/include/core/SkFontMetrics.h"
#include "third_party/skia/include/core/SkRect.h"
#include "utils/WindowsUtils.h"
class SkCanvas;
namespace txt {
using GlyphID = uint32_t;
// Constant with the unicode codepoint for the "Object replacement character".
// Used as a stand-in character for Placeholder boxes.
const int objReplacementChar = 0xFFFC;
// Constant with the unicode codepoint for the "Replacement character". This is
// the character that commonly renders as a black diamond with a white question
// mark. Used to replace non-placeholder instances of 0xFFFC in the text buffer.
const int replacementChar = 0xFFFD;
// Paragraph provides Layout, metrics, and painting capabilities for text. Once
// a Paragraph is constructed with ParagraphBuilder::Build(), an example basic
// workflow can be this:
//
// std::unique_ptr<Paragraph> paragraph = paragraph_builder.Build();
// paragraph->Layout(<somewidthgoeshere>);
// paragraph->Paint(<someSkCanvas>, <xpos>, <ypos>);
class Paragraph {
public:
// Constructor. It is highly recommended to construct a paragraph with a
// ParagraphBuilder.
Paragraph();
~Paragraph();
enum Affinity { UPSTREAM, DOWNSTREAM };
// Options for various types of bounding boxes provided by
// GetRectsForRange(...).
enum class RectHeightStyle {
// Provide tight bounding boxes that fit heights per run.
kTight,
// The height of the boxes will be the maximum height of all runs in the
// line. All rects in the same line will be the same height.
kMax,
// Extends the top and/or bottom edge of the bounds to fully cover any line
// spacing. The top edge of each line should be the same as the bottom edge
// of the line above. There should be no gaps in vertical coverage given any
// ParagraphStyle line_height.
//
// The top and bottom of each rect will cover half of the
// space above and half of the space below the line.
kIncludeLineSpacingMiddle,
// The line spacing will be added to the top of the rect.
kIncludeLineSpacingTop,
// The line spacing will be added to the bottom of the rect.
kIncludeLineSpacingBottom,
// Calculate boxes based on the strut's metrics.
kStrut
};
enum class RectWidthStyle {
// Provide tight bounding boxes that fit widths to the runs of each line
// independently.
kTight,
// Extends the width of the last rect of each line to match the position of
// the widest rect over all the lines.
kMax
};
struct PositionWithAffinity {
const size_t position;
const Affinity affinity;
PositionWithAffinity(size_t p, Affinity a) : position(p), affinity(a) {}
};
struct TextBox {
SkRect rect;
TextDirection direction;
TextBox(SkRect r, TextDirection d) : rect(r), direction(d) {}
};
template <typename T>
struct Range {
Range() : start(), end() {}
Range(T s, T e) : start(s), end(e) {}
T start, end;
bool operator==(const Range<T>& other) const {
return start == other.start && end == other.end;
}
T width() const { return end - start; }
void Shift(T delta) {
start += delta;
end += delta;
}
};
// Minikin Layout doLayout() and LineBreaker addStyleRun() has an
// O(N^2) (according to benchmarks) time complexity where N is the total
// number of characters. However, this is not significant for reasonably sized
// paragraphs. It is currently recommended to break up very long paragraphs
// (10k+ characters) to ensure speedy layout.
//
// Layout calculates the positioning of all the glyphs. Must call this method
// before Painting and getting any statistics from this class.
void Layout(double width, bool force = false);
// Paints the Laid out text onto the supplied SkCanvas at (x, y) offset from
// the origin. Only valid after Layout() is called.
void Paint(SkCanvas* canvas, double x, double y);
// Getter for paragraph_style_.
const ParagraphStyle& GetParagraphStyle() const;
// Returns the number of characters/unicode characters. AKA text_.size()
size_t TextSize() const;
// Returns the height of the laid out paragraph. NOTE this is not a tight
// bounding height of the glyphs, as some glyphs do not reach as low as they
// can.
double GetHeight() const;
// Returns the width provided in the Layout() method. This is the maximum
// width any line in the laid out paragraph can occupy. We expect that
// GetMaxWidth() >= GetLayoutWidth().
double GetMaxWidth() const;
// Returns the width of the longest line as found in Layout(), which is
// defined as the horizontal distance from the left edge of the leftmost glyph
// to the right edge of the rightmost glyph. We expect that
// GetLongestLine() <= GetMaxWidth().
double GetLongestLine() const;
// Distance from top of paragraph to the Alphabetic baseline of the first
// line. Used for alphabetic fonts (A-Z, a-z, greek, etc.)
double GetAlphabeticBaseline() const;
// Distance from top of paragraph to the Ideographic baseline of the first
// line. Used for ideographic fonts (Chinese, Japanese, Korean, etc.)
double GetIdeographicBaseline() const;
// Returns the total width covered by the paragraph without linebreaking.
double GetMaxIntrinsicWidth() const;
// Currently, calculated similarly to as GetLayoutWidth(), however this is not
// nessecarily 100% correct in all cases.
//
// Returns the actual max width of the longest line after Layout().
double GetMinIntrinsicWidth() const;
// Returns a vector of bounding boxes that enclose all text between start and
// end glyph indexes, including start and excluding end.
std::vector<TextBox> GetRectsForRange(size_t start,
size_t end,
RectHeightStyle rect_height_style,
RectWidthStyle rect_width_style) const;
// Returns the index of the glyph that corresponds to the provided coordinate,
// with the top left corner as the origin, and +y direction as down.
PositionWithAffinity GetGlyphPositionAtCoordinate(double dx, double dy) const;
// Returns a vector of bounding boxes that bound all inline placeholders in
// the paragraph.
//
// There will be one box for each inline placeholder. The boxes will be in the
// same order as they were added to the paragraph. The bounds will always be
// tight and should fully enclose the area where the placeholder should be.
//
// More granular boxes may be obtained through GetRectsForRange, which will
// return bounds on both text as well as inline placeholders.
std::vector<Paragraph::TextBox> GetRectsForPlaceholders() const;
// Finds the first and last glyphs that define a word containing the glyph at
// index offset.
Range<size_t> GetWordBoundary(size_t offset) const;
// Returns the number of lines the paragraph takes up. If the text exceeds the
// amount width and maxlines provides, Layout() truncates the extra text from
// the layout and this will return the max lines allowed.
size_t GetLineCount() const;
// Checks if the layout extends past the maximum lines and had to be
// truncated.
bool DidExceedMaxLines() const;
// Sets the needs_layout_ to dirty. When Layout() is called, a new Layout will
// be performed when this is set to true. Can also be used to prevent a new
// Layout from being calculated by setting to false.
void SetDirty(bool dirty = true);
private:
friend class ParagraphBuilder;
FRIEND_TEST(ParagraphTest, SimpleParagraph);
FRIEND_TEST(ParagraphTest, SimpleRedParagraph);
FRIEND_TEST(ParagraphTest, RainbowParagraph);
FRIEND_TEST(ParagraphTest, DefaultStyleParagraph);
FRIEND_TEST(ParagraphTest, BoldParagraph);
FRIEND_TEST_WINDOWS_DISABLED(ParagraphTest, LeftAlignParagraph);
FRIEND_TEST_WINDOWS_DISABLED(ParagraphTest, RightAlignParagraph);
FRIEND_TEST_WINDOWS_DISABLED(ParagraphTest, CenterAlignParagraph);
FRIEND_TEST_WINDOWS_DISABLED(ParagraphTest, JustifyAlignParagraph);
FRIEND_TEST_WINDOWS_DISABLED(ParagraphTest, JustifyRTL);
FRIEND_TEST(ParagraphTest, DecorationsParagraph);
FRIEND_TEST(ParagraphTest, ItalicsParagraph);
FRIEND_TEST(ParagraphTest, ChineseParagraph);
FRIEND_TEST(ParagraphTest, DISABLED_ArabicParagraph);
FRIEND_TEST(ParagraphTest, SpacingParagraph);
FRIEND_TEST(ParagraphTest, LongWordParagraph);
FRIEND_TEST(ParagraphTest, KernScaleParagraph);
FRIEND_TEST_WINDOWS_DISABLED(ParagraphTest, NewlineParagraph);
FRIEND_TEST_WINDOWS_DISABLED(ParagraphTest, EmojiParagraph);
FRIEND_TEST(ParagraphTest, HyphenBreakParagraph);
FRIEND_TEST(ParagraphTest, RepeatLayoutParagraph);
FRIEND_TEST(ParagraphTest, Ellipsize);
FRIEND_TEST(ParagraphTest, UnderlineShiftParagraph);
FRIEND_TEST(ParagraphTest, SimpleShadow);
FRIEND_TEST(ParagraphTest, ComplexShadow);
FRIEND_TEST(ParagraphTest, FontFallbackParagraph);
FRIEND_TEST(ParagraphTest, InlinePlaceholder0xFFFCParagraph);
FRIEND_TEST(ParagraphTest, FontFeaturesParagraph);
// Starting data to layout.
std::vector<uint16_t> text_;
// A vector of PlaceholderRuns, which detail the sizes, positioning and break
// behavior of the empty spaces to leave. Each placeholder span corresponds to
// a 0xFFFC (object replacement character) in text_, which indicates the
// position in the text where the placeholder will occur. There should be an
// equal number of 0xFFFC characters and elements in this vector.
std::vector<PlaceholderRun> inline_placeholders_;
// The indexes of the boxes that correspond to an inline placeholder.
std::vector<size_t> inline_placeholder_boxes_;
// The indexes of instances of 0xFFFC that correspond to placeholders. This is
// necessary since the user may pass in manually entered 0xFFFC values using
// AddText().
std::unordered_set<size_t> obj_replacement_char_indexes_;
StyledRuns runs_;
ParagraphStyle paragraph_style_;
std::shared_ptr<FontCollection> font_collection_;
minikin::LineBreaker breaker_;
mutable std::unique_ptr<icu::BreakIterator> word_breaker_;
struct LineRange {
LineRange(size_t s, size_t e, size_t eew, size_t ein, bool h)
: start(s),
end(e),
end_excluding_whitespace(eew),
end_including_newline(ein),
hard_break(h) {}
size_t start, end;
size_t end_excluding_whitespace;
size_t end_including_newline;
bool hard_break;
};
std::vector<LineRange> line_ranges_;
std::vector<double> line_widths_;
// Stores the result of Layout().
std::vector<PaintRecord> records_;
std::vector<double> line_heights_;
std::vector<double> line_baselines_;
bool did_exceed_max_lines_;
// Strut metrics of zero will have no effect on the layout.
struct StrutMetrics {
double ascent = 0; // Positive value to keep signs clear.
double descent = 0;
double leading = 0;
double half_leading = 0;
double line_height = 0;
bool force_strut = false;
};
StrutMetrics strut_;
// Metrics for use in GetRectsForRange(...);
// Per-line max metrics over all runs in a given line.
std::vector<SkScalar> line_max_spacings_;
std::vector<SkScalar> line_max_descent_;
std::vector<SkScalar> line_max_ascent_;
// Overall left and right extremes over all lines.
double max_right_;
double min_left_;
class BidiRun {
public:
// Constructs a BidiRun with is_ghost defaulted to false.
BidiRun(size_t s, size_t e, TextDirection d, const TextStyle& st)
: start_(s), end_(e), direction_(d), style_(&st), is_ghost_(false) {}
// Constructs a BidiRun with a custom is_ghost flag.
BidiRun(size_t s,
size_t e,
TextDirection d,
const TextStyle& st,
bool is_ghost)
: start_(s), end_(e), direction_(d), style_(&st), is_ghost_(is_ghost) {}
// Constructs a placeholder bidi run.
BidiRun(size_t s,
size_t e,
TextDirection d,
const TextStyle& st,
PlaceholderRun& placeholder)
: start_(s),
end_(e),
direction_(d),
style_(&st),
placeholder_run_(&placeholder) {}
size_t start() const { return start_; }
size_t end() const { return end_; }
size_t size() const { return end_ - start_; }
TextDirection direction() const { return direction_; }
const TextStyle& style() const { return *style_; }
PlaceholderRun* placeholder_run() const { return placeholder_run_; }
bool is_rtl() const { return direction_ == TextDirection::rtl; }
// Tracks if the run represents trailing whitespace.
bool is_ghost() const { return is_ghost_; }
bool is_placeholder_run() const { return placeholder_run_ != nullptr; }
private:
size_t start_, end_;
TextDirection direction_;
const TextStyle* style_;
bool is_ghost_;
PlaceholderRun* placeholder_run_ = nullptr;
};
struct GlyphPosition {
Range<size_t> code_units;
Range<double> x_pos;
GlyphPosition(double x_start,
double x_advance,
size_t code_unit_index,
size_t code_unit_width);
void Shift(double delta);
};
struct GlyphLine {
// Glyph positions sorted by x coordinate.
const std::vector<GlyphPosition> positions;
const size_t total_code_units;
GlyphLine(std::vector<GlyphPosition>&& p, size_t tcu);
};
struct CodeUnitRun {
// Glyph positions sorted by code unit index.
std::vector<GlyphPosition> positions;
Range<size_t> code_units;
Range<double> x_pos;
size_t line_number;
SkFontMetrics font_metrics;
TextDirection direction;
const PlaceholderRun* placeholder_run;
CodeUnitRun(std::vector<GlyphPosition>&& p,
Range<size_t> cu,
Range<double> x,
size_t line,
const SkFontMetrics& metrics,
TextDirection dir,
const PlaceholderRun* placeholder);
void Shift(double delta);
};
// Holds the laid out x positions of each glyph.
std::vector<GlyphLine> glyph_lines_;
// Holds the positions of each range of code units in the text.
// Sorted in code unit index order.
std::vector<CodeUnitRun> code_unit_runs_;
// Holds the positions of the inline placeholders.
std::vector<CodeUnitRun> inline_placeholder_code_unit_runs_;
// The max width of the paragraph as provided in the most recent Layout()
// call.
double width_ = -1.0f;
double longest_line_ = -1.0f;
double max_intrinsic_width_ = 0;
double min_intrinsic_width_ = 0;
double alphabetic_baseline_ = FLT_MAX;
double ideographic_baseline_ = FLT_MAX;
bool needs_layout_ = true;
struct WaveCoordinates {
double x_start;
double y_start;
double x_end;
double y_end;
WaveCoordinates(double x_s, double y_s, double x_e, double y_e)
: x_start(x_s), y_start(y_s), x_end(x_e), y_end(y_e) {}
};
// Passes in the text and Styled Runs. text_ and runs_ will later be passed
// into breaker_ in InitBreaker(), which is called in Layout().
void SetText(std::vector<uint16_t> text, StyledRuns runs);
void SetParagraphStyle(const ParagraphStyle& style);
void SetFontCollection(std::shared_ptr<FontCollection> font_collection);
void SetInlinePlaceholders(
std::vector<PlaceholderRun> inline_placeholders,
std::unordered_set<size_t> obj_replacement_char_indexes);
// Break the text into lines.
bool ComputeLineBreaks();
// Break the text into runs based on LTR/RTL text direction.
bool ComputeBidiRuns(std::vector<BidiRun>* result);
// Calculates and populates strut based on paragraph_style_ strut info.
void ComputeStrut(StrutMetrics* strut, SkFont& font);
// Adjusts the ascent and descent based on the existence and type of
// placeholder. This method sets the proper metrics to achieve the different
// PlaceholderAlignment options.
void ComputePlaceholder(PlaceholderRun* placeholder_run,
double& ascent,
double& descent);
bool IsStrutValid() const;
// Calculate the starting X offset of a line based on the line's width and
// alignment.
double GetLineXOffset(double line_total_advance);
// Creates and draws the decorations onto the canvas.
void PaintDecorations(SkCanvas* canvas,
const PaintRecord& record,
SkPoint base_offset);
// Draws the background onto the canvas.
void PaintBackground(SkCanvas* canvas,
const PaintRecord& record,
SkPoint base_offset);
// Draws the shadows onto the canvas.
void PaintShadow(SkCanvas* canvas, const PaintRecord& record, SkPoint offset);
// Obtain a Minikin font collection matching this text style.
std::shared_ptr<minikin::FontCollection> GetMinikinFontCollectionForStyle(
const TextStyle& style);
// Get a default SkTypeface for a text style.
sk_sp<SkTypeface> GetDefaultSkiaTypeface(const TextStyle& style);
FML_DISALLOW_COPY_AND_ASSIGN(Paragraph);
};
} // namespace txt
#endif // LIB_TXT_SRC_PARAGRAPH_H_