diff options
author | ckocagil@chromium.org <ckocagil@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-09-16 20:16:20 +0000 |
---|---|---|
committer | ckocagil@chromium.org <ckocagil@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-09-16 20:16:20 +0000 |
commit | eced6cba57f61829cfa095499f9c6f0c94d6594b (patch) | |
tree | af6e23bff7d55d84625213a80fd7f88c902c51ba /ui/gfx | |
parent | bd7189299af5db10ef53bac5f6a13a0dfdce72b8 (diff) | |
download | chromium_src-eced6cba57f61829cfa095499f9c6f0c94d6594b.zip chromium_src-eced6cba57f61829cfa095499f9c6f0c94d6594b.tar.gz chromium_src-eced6cba57f61829cfa095499f9c6f0c94d6594b.tar.bz2 |
Windows implementation of multiline RenderText
Multi-line text rendering in the Chromium UI is currently done by slicing the string into multiple lines by using text metrics from cross-platform libraries and rendering text by using different RenderText instances - one for each line. This approach has poor performance and is very bug-prone.
This CL adds a cross-platform interface and data structures to RenderText to support multi-line text rendering. This CL also implements the required platform-specific bits for Windows. Support for other platforms will be implemented in subsequent CLs.
Multi-line rendering as implemented in this CL is limited. Newline characters are ignored. RTL and complex scripts are supported, while there are be issues with selection highlights. Text-space <-> view-space mappings do not support RTL/complex scripts.
BUG=248597
Review URL: https://chromiumcodereview.appspot.com/16867016
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@223394 0039d316-1c4b-4281-b951-d872f2087c98
Diffstat (limited to 'ui/gfx')
-rw-r--r-- | ui/gfx/break_list.h | 1 | ||||
-rw-r--r-- | ui/gfx/render_text.cc | 124 | ||||
-rw-r--r-- | ui/gfx/render_text.h | 91 | ||||
-rw-r--r-- | ui/gfx/render_text_linux.cc | 2 | ||||
-rw-r--r-- | ui/gfx/render_text_mac.cc | 4 | ||||
-rw-r--r-- | ui/gfx/render_text_unittest.cc | 95 | ||||
-rw-r--r-- | ui/gfx/render_text_win.cc | 465 | ||||
-rw-r--r-- | ui/gfx/render_text_win.h | 11 |
8 files changed, 675 insertions, 118 deletions
diff --git a/ui/gfx/break_list.h b/ui/gfx/break_list.h index 932d545..4b549fc 100644 --- a/ui/gfx/break_list.h +++ b/ui/gfx/break_list.h @@ -44,6 +44,7 @@ class BreakList { // Set the max position and trim any breaks at or beyond that position. void SetMax(size_t max); + size_t max() const { return max_; } // Get the break applicable to |position| (at or preceeding |position|). typename std::vector<Break>::iterator GetBreak(size_t position); diff --git a/ui/gfx/render_text.cc b/ui/gfx/render_text.cc index c4335b4..5f47528 100644 --- a/ui/gfx/render_text.cc +++ b/ui/gfx/render_text.cc @@ -301,6 +301,14 @@ void StyleIterator::UpdatePosition(size_t position) { style_[i] = styles_[i].GetBreak(position); } +LineSegment::LineSegment() : run(0) {} + +LineSegment::~LineSegment() {} + +Line::Line() : preceding_heights(0), baseline(0) {} + +Line::~Line() {} + } // namespace internal RenderText::~RenderText() { @@ -308,6 +316,8 @@ RenderText::~RenderText() { void RenderText::SetText(const base::string16& text) { DCHECK(!composition_range_.IsValid()); + if (text_ == text) + return; text_ = text; // Adjust ranged styles and colors to accommodate a new text length. @@ -394,9 +404,18 @@ void RenderText::SetObscuredRevealIndex(int index) { ResetLayout(); } +void RenderText::SetMultiline(bool multiline) { + if (multiline != multiline_) { + multiline_ = multiline; + cached_bounds_and_offset_valid_ = false; + lines_.clear(); + } +} + void RenderText::SetDisplayRect(const Rect& r) { display_rect_ = r; cached_bounds_and_offset_valid_ = false; + lines_.clear(); } void RenderText::SetCursorPosition(size_t position) { @@ -687,6 +706,10 @@ void RenderText::DrawSelectedTextForDrag(Canvas* canvas) { Rect RenderText::GetCursorBounds(const SelectionModel& caret, bool insert_mode) { + // TODO(ckocagil): Support multiline. This function should return the height + // of the line the cursor is on. |GetStringSize()| now returns + // the multiline size, eliminate its use here. + EnsureLayout(); size_t caret_pos = caret.caret_pos(); @@ -776,6 +799,7 @@ RenderText::RenderText() obscured_(false), obscured_reveal_index_(-1), truncate_length_(0), + multiline_(false), fade_head_(false), fade_tail_(false), background_is_transparent_(false), @@ -819,6 +843,26 @@ const base::string16& RenderText::GetLayoutText() const { return layout_text_.empty() ? text_ : layout_text_; } +const BreakList<size_t>& RenderText::GetLineBreaks() { + if (line_breaks_.max() != 0) + return line_breaks_; + + const string16& layout_text = GetLayoutText(); + const size_t text_length = layout_text.length(); + line_breaks_.SetValue(0); + line_breaks_.SetMax(text_length); + base::i18n::BreakIterator iter(layout_text, + base::i18n::BreakIterator::BREAK_LINE); + const bool success = iter.Init(); + DCHECK(success); + if (success) { + do { + line_breaks_.ApplyValue(iter.pos(), Range(iter.pos(), text_length)); + } while (iter.Advance()); + } + return line_breaks_; +} + void RenderText::ApplyCompositionAndSelectionStyles() { // Save the underline and color breaks to undo the temporary styles later. DCHECK(!composition_and_selection_styles_applied_); @@ -845,25 +889,81 @@ void RenderText::UndoCompositionAndSelectionStyles() { composition_and_selection_styles_applied_ = false; } -Vector2d RenderText::GetTextOffset() { +Vector2d RenderText::GetLineOffset(size_t line_number) { Vector2d offset = display_rect().OffsetFromOrigin(); - offset.Add(GetUpdatedDisplayOffset()); - offset.Add(GetAlignmentOffset()); + // TODO(ckocagil): Apply the display offset for multiline scrolling. + if (!multiline()) + offset.Add(GetUpdatedDisplayOffset()); + else + offset.Add(Vector2d(0, lines_[line_number].preceding_heights)); + offset.Add(GetAlignmentOffset(line_number)); return offset; } Point RenderText::ToTextPoint(const Point& point) { - return point - GetTextOffset(); + return point - GetLineOffset(0); + // TODO(ckocagil): Convert multiline view space points to text space. } Point RenderText::ToViewPoint(const Point& point) { - return point + GetTextOffset(); + if (!multiline()) + return point + GetLineOffset(0); + + // TODO(ckocagil): Traverse individual line segments for RTL support. + DCHECK(!lines_.empty()); + int x = point.x(); + size_t line = 0; + for (; line < lines_.size() && x > lines_[line].size.width(); ++line) + x -= lines_[line].size.width(); + return Point(x, point.y()) + GetLineOffset(line); } -Vector2d RenderText::GetAlignmentOffset() { +std::vector<Rect> RenderText::TextBoundsToViewBounds(const Range& x) { + std::vector<Rect> rects; + + if (!multiline()) { + rects.push_back(Rect(ToViewPoint(Point(x.GetMin(), 0)), + Size(x.length(), GetStringSize().height()))); + return rects; + } + + EnsureLayout(); + + // Each line segment keeps its position in text coordinates. Traverse all line + // segments and if the segment intersects with the given range, add the view + // rect corresponding to the intersection to |rects|. + for (size_t line = 0; line < lines_.size(); ++line) { + int line_x = 0; + const Vector2d offset = GetLineOffset(line); + for (size_t i = 0; i < lines_[line].segments.size(); ++i) { + const internal::LineSegment* segment = &lines_[line].segments[i]; + const Range intersection = segment->x_range.Intersect(x); + if (!intersection.is_empty()) { + Rect rect(line_x + intersection.start() - segment->x_range.start(), + 0, intersection.length(), lines_[line].size.height()); + rects.push_back(rect + offset); + } + line_x += segment->x_range.length(); + } + } + + return rects; +} + +Vector2d RenderText::GetAlignmentOffset(size_t line_number) { + // TODO(ckocagil): Enable |lines_| usage in other platforms. +#if defined(OS_WIN) + DCHECK_LT(line_number, lines_.size()); +#endif Vector2d offset; if (horizontal_alignment_ != ALIGN_LEFT) { - offset.set_x(display_rect().width() - GetContentWidth()); +#if defined(OS_WIN) + const int width = lines_[line_number].size.width() + + (cursor_enabled_ ? 1 : 0); +#else + const int width = GetContentWidth(); +#endif + offset.set_x(display_rect().width() - width); if (horizontal_alignment_ == ALIGN_CENTER) offset.set_x(offset.x() / 2); } @@ -876,14 +976,13 @@ Vector2d RenderText::GetAlignmentOffset() { } void RenderText::ApplyFadeEffects(internal::SkiaTextRenderer* renderer) { - if (!fade_head() && !fade_tail()) + if (multiline() || (!fade_head() && !fade_tail())) return; - const int text_width = GetStringSize().width(); const int display_width = display_rect().width(); // If the text fits as-is, no need to fade. - if (text_width <= display_width) + if (GetStringSize().width() <= display_width) return; int gradient_width = CalculateFadeGradientWidth(GetPrimaryFont(), @@ -915,7 +1014,7 @@ void RenderText::ApplyFadeEffects(internal::SkiaTextRenderer* renderer) { } Rect text_rect = display_rect(); - text_rect.Inset(GetAlignmentOffset().x(), 0, 0, 0); + text_rect.Inset(GetAlignmentOffset(0).x(), 0, 0, 0); // TODO(msw): Use the actual text colors corresponding to each faded part. skia::RefPtr<SkShader> shader = CreateFadeShader( @@ -949,6 +1048,7 @@ void RenderText::MoveCursorTo(size_t position, bool select) { void RenderText::UpdateLayoutText() { layout_text_.clear(); + line_breaks_.SetMax(0); if (obscured_) { size_t obscured_text_length = @@ -985,6 +1085,8 @@ void RenderText::UpdateCachedBoundsAndOffset() { if (cached_bounds_and_offset_valid_) return; + // TODO(ckocagil): Add support for scrolling multiline text. + // First, set the valid flag true to calculate the current cursor bounds using // the stale |display_offset_|. Applying |delta_offset| at the end of this // function will set |cursor_bounds_| and |display_offset_| to correct values. diff --git a/ui/gfx/render_text.h b/ui/gfx/render_text.h index 1fc8ed7..fe4720f 100644 --- a/ui/gfx/render_text.h +++ b/ui/gfx/render_text.h @@ -111,6 +111,40 @@ class StyleIterator { DISALLOW_COPY_AND_ASSIGN(StyleIterator); }; +// Line segments are slices of the layout text to be rendered on a single line. +struct LineSegment { + LineSegment(); + ~LineSegment(); + + // X coordinates of this line segment in text space. + Range x_range; + + // The character range this segment corresponds to. + Range char_range; + + // Index of the text run that generated this segment. + size_t run; +}; + +// A line of layout text, comprised of a line segment list and some metrics. +struct Line { + Line(); + ~Line(); + + // Segments that make up this line in visual order. + std::vector<LineSegment> segments; + + // A line size is the sum of segment widths and the maximum of segment + // heights. + Size size; + + // Sum of preceding lines' heights. + int preceding_heights; + + // Maximum baseline of all segments on this line. + int baseline; +}; + } // namespace internal // RenderText represents an abstract model of styled text and its corresponding @@ -186,6 +220,11 @@ class UI_EXPORT RenderText { // cleared when SetText or SetObscured is called. void SetObscuredRevealIndex(int index); + // TODO(ckocagil): Multiline text rendering is currently only supported on + // Windows. Support other platforms. + bool multiline() const { return multiline_; } + void SetMultiline(bool multiline); + // Set the maximum length of the displayed layout text, not the actual text. // A |length| of 0 forgoes a hard limit, but does not guarantee proper // functionality of very long strings. Applies to subsequent SetText calls. @@ -281,14 +320,14 @@ class UI_EXPORT RenderText { // |GetTextDirection()|, not the direction of a particular run. VisualCursorDirection GetVisualDirectionOfLogicalEnd(); - // Returns the size in pixels of the entire string. For the height, this will - // return the maximum height among the different fonts in the text runs. - // Note that this returns the raw size of the string, which does not include - // the margin area of text shadows. + // Returns the size required to display the current string (which is the + // wrapped size in multiline mode). Note that this returns the raw size of the + // string, which does not include the cursor or the margin area of text + // shadows. virtual Size GetStringSize() = 0; - // Returns the width of content, which reserves room for the cursor if - // |cursor_enabled_| is true. + // Returns the width of the content (which is the wrapped width in multiline + // mode). Reserves room for the cursor if |cursor_enabled_| is true. int GetContentWidth(); // Returns the common baseline of the text. The returned value is the vertical @@ -348,6 +387,9 @@ class UI_EXPORT RenderText { const BreakList<SkColor>& colors() const { return colors_; } const std::vector<BreakList<bool> >& styles() const { return styles_; } + const std::vector<internal::Line>& lines() const { return lines_; } + void set_lines(std::vector<internal::Line>* lines) { lines_.swap(*lines); } + const Vector2d& GetUpdatedDisplayOffset(); void set_cached_bounds_and_offset_valid(bool valid) { @@ -406,7 +448,7 @@ class UI_EXPORT RenderText { // Reset the layout to be invalid. virtual void ResetLayout() = 0; - // Ensure the text is laid out. + // Ensure the text is laid out, lines are computed, and |lines_| is valid. virtual void EnsureLayout() = 0; // Draw the text. @@ -415,22 +457,29 @@ class UI_EXPORT RenderText { // Returns the text used for layout, which may be obscured or truncated. const base::string16& GetLayoutText() const; + // Returns layout text positions that are suitable for breaking lines. + const BreakList<size_t>& GetLineBreaks(); + // Apply (and undo) temporary composition underlines and selection colors. void ApplyCompositionAndSelectionStyles(); void UndoCompositionAndSelectionStyles(); - // Returns the text offset from the origin after applying text alignment and - // display offset. - Vector2d GetTextOffset(); + // Returns the line offset from the origin after applying the text alignment + // and the display offset. + Vector2d GetLineOffset(size_t line_number); - // Convert points from the text space to the view space and back. - // Handles the display area, display offset, and the application LTR/RTL mode. + // Convert points from the text space to the view space and back. Handles the + // display area, display offset, application LTR/RTL mode and multiline. Point ToTextPoint(const Point& point); Point ToViewPoint(const Point& point); - // Returns the text offset from the origin, taking into account text alignment + // Convert a text space x-coordinate range to corresponding rects in view + // space. + std::vector<Rect> TextBoundsToViewBounds(const Range& x); + + // Returns the line offset from the origin, accounting for text alignment // only. - Vector2d GetAlignmentOffset(); + Vector2d GetAlignmentOffset(size_t line_number); // Applies fade effects to |renderer|. void ApplyFadeEffects(internal::SkiaTextRenderer* renderer); @@ -457,6 +506,9 @@ class UI_EXPORT RenderText { FRIEND_TEST_ALL_PREFIXES(RenderTextTest, EdgeSelectionModels); FRIEND_TEST_ALL_PREFIXES(RenderTextTest, GetTextOffset); FRIEND_TEST_ALL_PREFIXES(RenderTextTest, GetTextOffsetHorizontalDefaultInRTL); + FRIEND_TEST_ALL_PREFIXES(RenderTextTest, Multiline_MinWidth); + FRIEND_TEST_ALL_PREFIXES(RenderTextTest, Multiline_NormalWidth); + FRIEND_TEST_ALL_PREFIXES(RenderTextTest, Multiline_SufficientWidth); // Set the cursor to |position|, with the caret trailing the previous // grapheme, or if there is no previous grapheme, leading the cursor position. @@ -547,6 +599,10 @@ class UI_EXPORT RenderText { // The obscured and/or truncated text that will be displayed. base::string16 layout_text_; + // Whether the text should be broken into multiple lines. Uses the width of + // |display_rect_| as the width cap. + bool multiline_; + // Fade text head and/or tail, if text doesn't fit into |display_rect_|. bool fade_head_; bool fade_tail_; @@ -574,6 +630,13 @@ class UI_EXPORT RenderText { // Text shadows to be drawn. ShadowValues text_shadows_; + // A list of valid layout text line break positions. + BreakList<size_t> line_breaks_; + + // Lines computed by EnsureLayout. These should be invalidated with + // ResetLayout and on |display_rect_| changes. + std::vector<internal::Line> lines_; + DISALLOW_COPY_AND_ASSIGN(RenderText); }; diff --git a/ui/gfx/render_text_linux.cc b/ui/gfx/render_text_linux.cc index 9ad2a3d..11bc487 100644 --- a/ui/gfx/render_text_linux.cc +++ b/ui/gfx/render_text_linux.cc @@ -376,7 +376,7 @@ void RenderTextLinux::DrawVisualText(Canvas* canvas) { DCHECK(layout_); // Skia will draw glyphs with respect to the baseline. - Vector2d offset(GetTextOffset() + Vector2d(0, GetBaseline())); + Vector2d offset(GetLineOffset(0) + Vector2d(0, GetBaseline())); SkScalar x = SkIntToScalar(offset.x()); SkScalar y = SkIntToScalar(offset.y()); diff --git a/ui/gfx/render_text_mac.cc b/ui/gfx/render_text_mac.cc index 61a85cd..39460b5 100644 --- a/ui/gfx/render_text_mac.cc +++ b/ui/gfx/render_text_mac.cc @@ -249,9 +249,9 @@ void RenderTextMac::ComputeRuns() { CFArrayRef ct_runs = CTLineGetGlyphRuns(line_); const CFIndex ct_runs_count = CFArrayGetCount(ct_runs); - // TODO(asvitkine): Don't use GetTextOffset() until draw time, since it may be + // TODO(asvitkine): Don't use GetLineOffset() until draw time, since it may be // updated based on alignment changes without resetting the layout. - gfx::Vector2d text_offset = GetTextOffset(); + gfx::Vector2d text_offset = GetLineOffset(0); // Skia will draw glyphs with respect to the baseline. text_offset += gfx::Vector2d(0, common_baseline_); diff --git a/ui/gfx/render_text_unittest.cc b/ui/gfx/render_text_unittest.cc index 625e3f0..b028e12 100644 --- a/ui/gfx/render_text_unittest.cc +++ b/ui/gfx/render_text_unittest.cc @@ -6,6 +6,7 @@ #include <algorithm> +#include "base/format_macros.h" #include "base/memory/scoped_ptr.h" #include "base/strings/stringprintf.h" #include "base/strings/utf_string_conversions.h" @@ -1294,7 +1295,7 @@ TEST_F(RenderTextTest, GetTextOffset) { Rect display_rect(font_size); render_text->SetDisplayRect(display_rect); - Vector2d offset = render_text->GetTextOffset(); + Vector2d offset = render_text->GetLineOffset(0); EXPECT_TRUE(offset.IsZero()); // Set display area's size greater than font size. @@ -1303,30 +1304,30 @@ TEST_F(RenderTextTest, GetTextOffset) { render_text->SetDisplayRect(display_rect); // Check the default horizontal and vertical alignment. - offset = render_text->GetTextOffset(); + offset = render_text->GetLineOffset(0); EXPECT_EQ(kEnlargement / 2, offset.y()); EXPECT_EQ(0, offset.x()); // Check explicitly setting the horizontal alignment. render_text->SetHorizontalAlignment(ALIGN_LEFT); - offset = render_text->GetTextOffset(); + offset = render_text->GetLineOffset(0); EXPECT_EQ(0, offset.x()); render_text->SetHorizontalAlignment(ALIGN_CENTER); - offset = render_text->GetTextOffset(); + offset = render_text->GetLineOffset(0); EXPECT_EQ(kEnlargement / 2, offset.x()); render_text->SetHorizontalAlignment(ALIGN_RIGHT); - offset = render_text->GetTextOffset(); + offset = render_text->GetLineOffset(0); EXPECT_EQ(kEnlargement, offset.x()); // Check explicitly setting the vertical alignment. render_text->SetVerticalAlignment(ALIGN_TOP); - offset = render_text->GetTextOffset(); + offset = render_text->GetLineOffset(0); EXPECT_EQ(0, offset.y()); render_text->SetVerticalAlignment(ALIGN_VCENTER); - offset = render_text->GetTextOffset(); + offset = render_text->GetLineOffset(0); EXPECT_EQ(kEnlargement / 2, offset.y()); render_text->SetVerticalAlignment(ALIGN_BOTTOM); - offset = render_text->GetTextOffset(); + offset = render_text->GetLineOffset(0); EXPECT_EQ(kEnlargement, offset.y()); SetRTL(was_rtl); @@ -1334,7 +1335,7 @@ TEST_F(RenderTextTest, GetTextOffset) { TEST_F(RenderTextTest, GetTextOffsetHorizontalDefaultInRTL) { // This only checks the default horizontal alignment in RTL mode; all other - // GetTextOffset() attributes are checked by the test above. + // GetLineOffset(0) attributes are checked by the test above. const bool was_rtl = base::i18n::IsRTL(); SetRTL(true); scoped_ptr<RenderText> render_text(RenderText::CreateInstance()); @@ -1345,7 +1346,7 @@ TEST_F(RenderTextTest, GetTextOffsetHorizontalDefaultInRTL) { render_text->GetStringSize().height()); Rect display_rect(font_size); render_text->SetDisplayRect(display_rect); - Vector2d offset = render_text->GetTextOffset(); + Vector2d offset = render_text->GetLineOffset(0); EXPECT_EQ(kEnlargement, offset.x()); SetRTL(was_rtl); } @@ -1652,6 +1653,80 @@ TEST_F(RenderTextTest, SelectionKeepsLigatures) { } #if defined(OS_WIN) +// Ensure strings wrap onto multiple lines for a small available width. +TEST_F(RenderTextTest, Multiline_MinWidth) { + const wchar_t* kTestStrings[] = { kWeak, kLtr, kLtrRtl, kLtrRtlLtr, kRtl, + kRtlLtr, kRtlLtrRtl }; + + scoped_ptr<RenderTextWin> render_text( + static_cast<RenderTextWin*>(RenderText::CreateInstance())); + render_text->SetDisplayRect(Rect(1, 1000)); + render_text->SetMultiline(true); + Canvas canvas; + + for (size_t i = 0; i < arraysize(kTestStrings); ++i) { + SCOPED_TRACE(base::StringPrintf("kTestStrings[%" PRIuS "]", i)); + render_text->SetText(WideToUTF16(kTestStrings[i])); + render_text->Draw(&canvas); + EXPECT_GT(render_text->lines_.size(), 1U); + } +} + +// Ensure strings wrap onto multiple lines for a normal available width. +TEST_F(RenderTextTest, Multiline_NormalWidth) { + const struct { + const wchar_t* const text; + const Range first_line_char_range; + const Range second_line_char_range; + } kTestStrings[] = { + { L"abc defg hijkl", Range(0, 9), Range(9, 14) }, + { L"qwertyuiop", Range(0, 8), Range(8, 10) }, + { L"\x062A\x0641\x0627\x062D\x05EA\x05E4\x05D5\x05D6\x05D9\x05DD", + Range(4, 10), Range(0, 4) } + }; + + scoped_ptr<RenderTextWin> render_text( + static_cast<RenderTextWin*>(RenderText::CreateInstance())); + render_text->SetDisplayRect(Rect(50, 1000)); + render_text->SetMultiline(true); + Canvas canvas; + + for (size_t i = 0; i < ARRAYSIZE_UNSAFE(kTestStrings); ++i) { + SCOPED_TRACE(base::StringPrintf("kTestStrings[%" PRIuS "]", i)); + render_text->SetText(WideToUTF16(kTestStrings[i].text)); + render_text->Draw(&canvas); + ASSERT_EQ(2U, render_text->lines_.size()); + ASSERT_EQ(1U, render_text->lines_[0].segments.size()); + EXPECT_EQ(kTestStrings[i].first_line_char_range, + render_text->lines_[0].segments[0].char_range); + ASSERT_EQ(1U, render_text->lines_[1].segments.size()); + EXPECT_EQ(kTestStrings[i].second_line_char_range, + render_text->lines_[1].segments[0].char_range); + } +} + +// Ensure strings don't wrap onto multiple lines for a sufficient available +// width. +TEST_F(RenderTextTest, Multiline_SufficientWidth) { + const wchar_t* kTestStrings[] = { L"", L" ", L".", L" . ", L"abc", L"a b c", + L"\x62E\x628\x632", L"\x62E \x628 \x632" }; + + scoped_ptr<RenderTextWin> render_text( + static_cast<RenderTextWin*>(RenderText::CreateInstance())); + render_text->SetDisplayRect(Rect(30, 1000)); + render_text->SetMultiline(true); + Canvas canvas; + + for (size_t i = 0; i < arraysize(kTestStrings); ++i) { + SCOPED_TRACE(base::StringPrintf("kTestStrings[%" PRIuS "]", i)); + render_text->SetText(WideToUTF16(kTestStrings[i])); + render_text->Draw(&canvas); + EXPECT_EQ(1U, render_text->lines_.size()); + } +} +#endif // defined(OS_WIN) + +#if defined(OS_WIN) TEST_F(RenderTextTest, Win_BreakRunsByUnicodeBlocks) { scoped_ptr<RenderTextWin> render_text( static_cast<RenderTextWin*>(RenderText::CreateInstance())); diff --git a/ui/gfx/render_text_win.cc b/ui/gfx/render_text_win.cc index 2eff381..f0e42bd 100644 --- a/ui/gfx/render_text_win.cc +++ b/ui/gfx/render_text_win.cc @@ -156,6 +156,96 @@ gfx::Range CharRangeToGlyphRange(const internal::TextRun& run, return result; } +// Starting from |start_char|, finds a suitable line break position at or before +// |available_width| using word break info from |breaks|. If |empty_line| is +// true, this function will not roll back to |start_char| and |*next_char| will +// be greater than |start_char| (to avoid constructing empty lines). +// TODO(ckocagil): Do not break ligatures and diacritics. +// TextRun::logical_clusters might help. +// TODO(ckocagil): We might have to reshape after breaking at ligatures. +// See whether resolving the TODO above resolves this too. +// TODO(ckocagil): Do not reserve width for whitespace at the end of lines. +void BreakRunAtWidth(const internal::TextRun& run, + const BreakList<size_t>& breaks, + size_t start_char, + int available_width, + bool empty_line, + int* width, + size_t* next_char) { + DCHECK(run.range.Contains(Range(start_char, start_char + 1))); + BreakList<size_t>::const_iterator word = breaks.GetBreak(start_char); + BreakList<size_t>::const_iterator next_word = word + 1; + // Width from |std::max(word->first, start_char)| to the current character. + int word_width = 0; + *width = 0; + + for (size_t i = start_char; i < run.range.end(); ++i) { + // |word| holds the word boundary at or before |i|, and |next_word| holds + // the word boundary right after |i|. Advance both |word| and |next_word| + // when |i| reaches |next_word|. + if (next_word != breaks.breaks().end() && i >= next_word->first) { + word = next_word++; + word_width = 0; + } + + Range glyph_range = CharRangeToGlyphRange(run, Range(i, i + 1)); + int char_width = 0; + for (size_t j = glyph_range.start(); j < glyph_range.end(); ++j) + char_width += run.advance_widths[j]; + + *width += char_width; + word_width += char_width; + + if (*width > available_width) { + if (!empty_line || word_width < *width) { + *width -= word_width; + *next_char = std::max(word->first, start_char); + } else if (char_width < *width) { + *width -= char_width; + *next_char = i; + } else { + *next_char = i + 1; + } + + return; + } + } + + *next_char = run.range.end(); +} + +// For segments in the same run, checks the continuity and order of |x_range| +// and |char_range| fields. +void CheckLineIntegrity(const std::vector<internal::Line>& lines, + const ScopedVector<internal::TextRun>& runs) { + size_t previous_segment_line = 0; + const internal::LineSegment* previous_segment = NULL; + + for (size_t i = 0; i < lines.size(); ++i) { + for (size_t j = 0; j < lines[i].segments.size(); ++j) { + const internal::LineSegment* segment = &lines[i].segments[j]; + internal::TextRun* run = runs[segment->run]; + + if (!previous_segment) { + previous_segment = segment; + } else if (runs[previous_segment->run] != run) { + previous_segment = NULL; + } else { + DCHECK_EQ(previous_segment->char_range.end(), + segment->char_range.start()); + if (!run->script_analysis.fRTL) { + DCHECK_EQ(previous_segment->x_range.end(), segment->x_range.start()); + } else { + DCHECK_EQ(segment->x_range.end(), previous_segment->x_range.start()); + } + + previous_segment = segment; + previous_segment_line = i; + } + } + } +} + } // namespace namespace internal { @@ -199,6 +289,183 @@ int GetGlyphXBoundary(const internal::TextRun* run, return run->preceding_run_widths + x; } +// Internal class to generate Line structures. If |multiline| is true, the text +// is broken into lines at |words| boundaries such that each line is no longer +// than |max_width|. If |multiline| is false, only outputs a single Line from +// the given runs. |min_baseline| and |min_height| are the minimum baseline and +// height for each line. +// TODO(ckocagil): Expose the interface of this class in the header and test +// this class directly. +class LineBreaker { + public: + LineBreaker(int max_width, + int min_baseline, + int min_height, + bool multiline, + const BreakList<size_t>* words, + const ScopedVector<TextRun>& runs) + : max_width_(max_width), + min_baseline_(min_baseline), + min_height_(min_height), + multiline_(multiline), + words_(words), + runs_(runs), + text_x_(0), + line_x_(0), + line_ascent_(0), + line_descent_(0) { + AdvanceLine(); + } + + // Breaks the run at given |run_index| into Line structs. + void AddRun(int run_index) { + const TextRun* run = runs_[run_index]; + if (multiline_ && line_x_ + run->width > max_width_) + BreakRun(run_index); + else + AddSegment(run_index, run->range, run->width); + } + + // Finishes line breaking and outputs the results. Can be called at most once. + void Finalize(std::vector<Line>* lines, Size* size) { + DCHECK(!lines_.empty()); + // Add an empty line to finish the line size calculation and remove it. + AdvanceLine(); + lines_.pop_back(); + *size = total_size_; + lines->swap(lines_); + } + + private: + // A (line index, segment index) pair that specifies a segment in |lines_|. + typedef std::pair<size_t, size_t> SegmentHandle; + + LineSegment* SegmentFromHandle(const SegmentHandle& handle) { + return &lines_[handle.first].segments[handle.second]; + } + + // Breaks a run into segments that fit in the last line in |lines_| and adds + // them. Adds a new Line to the back of |lines_| whenever a new segment can't + // be added without the Line's width exceeding |max_width_|. + void BreakRun(int run_index) { + DCHECK(words_); + const TextRun* const run = runs_[run_index]; + int width = 0; + size_t next_char = run->range.start(); + + // Break the run until it fits the current line. + while (next_char < run->range.end()) { + const size_t current_char = next_char; + BreakRunAtWidth(*run, *words_, current_char, max_width_ - line_x_, + line_x_ == 0, &width, &next_char); + AddSegment(run_index, Range(current_char, next_char), width); + if (next_char < run->range.end()) + AdvanceLine(); + } + } + + // RTL runs are broken in logical order but displayed in visual order. To find + // the text-space coordinate (where it would fall in a single-line text) + // |x_range| of RTL segments, segment widths are applied in reverse order. + // e.g. {[5, 10], [10, 40]} will become {[35, 40], [5, 35]}. + void UpdateRTLSegmentRanges() { + if (rtl_segments_.empty()) + return; + int x = SegmentFromHandle(rtl_segments_[0])->x_range.start(); + for (size_t i = rtl_segments_.size(); i > 0; --i) { + LineSegment* segment = SegmentFromHandle(rtl_segments_[i - 1]); + const size_t segment_width = segment->x_range.length(); + segment->x_range = Range(x, x + segment_width); + x += segment_width; + } + rtl_segments_.clear(); + } + + // Finishes the size calculations of the last Line in |lines_|. Adds a new + // Line to the back of |lines_|. + void AdvanceLine() { + if (!lines_.empty()) { + Line* line = &lines_.back(); + // TODO(ckocagil): Determine optimal multiline height behavior. + if (line_ascent_ + line_descent_ == 0) { + line_ascent_ = min_baseline_; + line_descent_ = min_height_ - min_baseline_; + } + // Set the single-line mode Line's metrics to be at least + // |RenderText::font_list()| to not break the current single-line code. + line_ascent_ = std::max(line_ascent_, min_baseline_); + line_descent_ = std::max(line_descent_, min_height_ - min_baseline_); + + line->baseline = line_ascent_; + line->size.set_height(line_ascent_ + line_descent_); + line->preceding_heights = total_size_.height(); + total_size_.set_height(total_size_.height() + line->size.height()); + total_size_.set_width(std::max(total_size_.width(), line->size.width())); + } + line_x_ = 0; + line_ascent_ = 0; + line_descent_ = 0; + lines_.push_back(Line()); + } + + // Adds a new segment with the given properties to |lines_.back()|. + void AddSegment(int run_index, Range char_range, int width) { + if (char_range.is_empty()) { + DCHECK_EQ(width, 0); + return; + } + const TextRun* run = runs_[run_index]; + line_ascent_ = std::max(line_ascent_, run->font.GetBaseline()); + line_descent_ = std::max(line_descent_, + run->font.GetHeight() - run->font.GetBaseline()); + + LineSegment segment; + segment.run = run_index; + segment.char_range = char_range; + segment.x_range = Range(text_x_, text_x_ + width); + + Line* line = &lines_.back(); + line->segments.push_back(segment); + line->size.set_width(line->size.width() + segment.x_range.length()); + if (run->script_analysis.fRTL) { + rtl_segments_.push_back(SegmentHandle(lines_.size() - 1, + line->segments.size() - 1)); + // If this is the last segment of an RTL run, reprocess the text-space x + // ranges of all segments from the run. + if (char_range.end() == run->range.end()) + UpdateRTLSegmentRanges(); + } + text_x_ += width; + line_x_ += width; + } + + const int max_width_; + const int min_baseline_; + const int min_height_; + const bool multiline_; + const BreakList<size_t>* const words_; + const ScopedVector<TextRun>& runs_; + + // Stores the resulting lines. + std::vector<Line> lines_; + + // Text space and line space x coordinates of the next segment to be added. + int text_x_; + int line_x_; + + // Size of the multiline text, not including the currently processed line. + Size total_size_; + + // Ascent and descent values of the current line, |lines_.back()|. + int line_ascent_; + int line_descent_; + + // The current RTL run segments, to be applied by |UpdateRTLSegmentRanges()|. + std::vector<SegmentHandle> rtl_segments_; + + DISALLOW_COPY_AND_ASSIGN(LineBreaker); +}; + } // namespace internal // static @@ -209,7 +476,6 @@ std::map<std::string, Font> RenderTextWin::successful_substitute_fonts_; RenderTextWin::RenderTextWin() : RenderText(), - common_baseline_(0), needs_layout_(false) { set_truncate_length(kMaxUniscribeTextLength); @@ -224,12 +490,12 @@ RenderTextWin::~RenderTextWin() { Size RenderTextWin::GetStringSize() { EnsureLayout(); - return string_size_; + return multiline_string_size_; } int RenderTextWin::GetBaseline() { EnsureLayout(); - return common_baseline_; + return lines()[0].baseline; } SelectionModel RenderTextWin::FindCursorPosition(const Point& point) { @@ -371,7 +637,7 @@ gfx::Range RenderTextWin::GetGlyphBounds(size_t index) { GetRunContainingCaret(SelectionModel(index, CURSOR_FORWARD)); // Return edge bounds if the index is invalid or beyond the layout text size. if (run_index >= runs_.size()) - return gfx::Range(string_size_.width()); + return Range(string_width_); internal::TextRun* run = runs_[run_index]; const size_t layout_index = TextIndexToLayoutIndex(index); return gfx::Range(GetGlyphXBoundary(run, layout_index, false), @@ -385,11 +651,12 @@ std::vector<Rect> RenderTextWin::GetSubstringBounds(const gfx::Range& range) { TextIndexToLayoutIndex(range.end())); DCHECK(gfx::Range(0, GetLayoutText().length()).Contains(layout_range)); - std::vector<Rect> bounds; + std::vector<Rect> rects; if (layout_range.is_empty()) - return bounds; + return rects; + std::vector<Range> bounds; - // Add a Rect for each run/selection intersection. + // Add a Range for each run/selection intersection. // TODO(msw): The bounds should probably not always be leading the range ends. for (size_t i = 0; i < runs_.size(); ++i) { const internal::TextRun* run = runs_[visual_to_logical_[i]]; @@ -398,17 +665,23 @@ std::vector<Rect> RenderTextWin::GetSubstringBounds(const gfx::Range& range) { DCHECK(!intersection.is_reversed()); gfx::Range range_x(GetGlyphXBoundary(run, intersection.start(), false), GetGlyphXBoundary(run, intersection.end(), false)); - Rect rect(range_x.GetMin(), 0, range_x.length(), run->font.GetHeight()); - rect.set_origin(ToViewPoint(rect.origin())); - // Union this with the last rect if they're adjacent. - if (!bounds.empty() && rect.SharesEdgeWith(bounds.back())) { - rect.Union(bounds.back()); + if (range_x.is_empty()) + continue; + range_x = Range(range_x.GetMin(), range_x.GetMax()); + // Union this with the last range if they're adjacent. + DCHECK(bounds.empty() || bounds.back().GetMax() <= range_x.GetMin()); + if (!bounds.empty() && bounds.back().GetMax() == range_x.GetMin()) { + range_x = Range(bounds.back().GetMin(), range_x.GetMax()); bounds.pop_back(); } - bounds.push_back(rect); + bounds.push_back(range_x); } } - return bounds; + for (size_t i = 0; i < bounds.size(); ++i) { + std::vector<Rect> current_rects = TextBoundsToViewBounds(bounds[i]); + rects.insert(rects.end(), current_rects.begin(), current_rects.end()); + } + return rects; } size_t RenderTextWin::TextIndexToLayoutIndex(size_t index) const { @@ -450,23 +723,40 @@ void RenderTextWin::ResetLayout() { } void RenderTextWin::EnsureLayout() { - if (!needs_layout_) - return; - // TODO(msw): Skip complex processing if ScriptIsComplex returns false. - ItemizeLogicalText(); - if (!runs_.empty()) - LayoutVisualText(); - needs_layout_ = false; + if (needs_layout_) { + // TODO(msw): Skip complex processing if ScriptIsComplex returns false. + ItemizeLogicalText(); + if (!runs_.empty()) + LayoutVisualText(); + needs_layout_ = false; + std::vector<internal::Line> lines; + set_lines(&lines); + } + + // Compute lines if they're not valid. This is separate from the layout steps + // above to avoid text layout and shaping when we resize |display_rect_|. + if (lines().empty()) { + DCHECK(!needs_layout_); + std::vector<internal::Line> lines; + internal::LineBreaker line_breaker(display_rect().width() - 1, + font_list().GetBaseline(), + font_list().GetHeight(), multiline(), + multiline() ? &GetLineBreaks() : NULL, + runs_); + for (size_t i = 0; i < runs_.size(); ++i) + line_breaker.AddRun(visual_to_logical_[i]); + line_breaker.Finalize(&lines, &multiline_string_size_); + DCHECK(!lines.empty()); +#ifndef NDEBUG + CheckLineIntegrity(lines, runs_); +#endif + set_lines(&lines); + } } void RenderTextWin::DrawVisualText(Canvas* canvas) { DCHECK(!needs_layout_); - - // Skia will draw glyphs with respect to the baseline. - Vector2d offset(GetTextOffset() + Vector2d(0, common_baseline_)); - - SkScalar x = SkIntToScalar(offset.x()); - SkScalar y = SkIntToScalar(offset.y()); + DCHECK(!lines().empty()); std::vector<SkPoint> pos; @@ -483,52 +773,79 @@ void RenderTextWin::DrawVisualText(Canvas* canvas) { ApplyCompositionAndSelectionStyles(); - for (size_t i = 0; i < runs_.size(); ++i) { - // Get the run specified by the visual-to-logical map. - internal::TextRun* run = runs_[visual_to_logical_[i]]; + for (size_t i = 0; i < lines().size(); ++i) { + const internal::Line& line = lines()[i]; + const Vector2d line_offset = GetLineOffset(i); - // Skip painting empty runs and runs outside the display rect area. - if ((run->glyph_count == 0) || (x >= display_rect().right()) || - (x + run->width <= display_rect().x())) { - x += run->width; + // Skip painting empty lines or lines outside the display rect area. + if (!display_rect().Intersects(Rect(PointAtOffsetFromOrigin(line_offset), + line.size))) continue; - } - // Based on WebCore::skiaDrawText. |pos| contains the positions of glyphs. - // An extra terminal |pos| entry is added to simplify width calculations. - pos.resize(run->glyph_count + 1); - SkScalar glyph_x = x; - for (int glyph = 0; glyph < run->glyph_count; glyph++) { - pos[glyph].set(glyph_x + run->offsets[glyph].du, - y + run->offsets[glyph].dv); - glyph_x += SkIntToScalar(run->advance_widths[glyph]); - } - pos.back().set(glyph_x, y); - - renderer.SetTextSize(run->font.GetFontSize()); - renderer.SetFontFamilyWithStyle(run->font.GetFontName(), run->font_style); - - for (BreakList<SkColor>::const_iterator it = - colors().GetBreak(run->range.start()); - it != colors().breaks().end() && it->first < run->range.end(); - ++it) { - const gfx::Range glyph_range = CharRangeToGlyphRange(*run, - colors().GetRange(it).Intersect(run->range)); - if (glyph_range.is_empty()) - continue; - renderer.SetForegroundColor(it->second); - renderer.DrawPosText(&pos[glyph_range.start()], - &run->glyphs[glyph_range.start()], - glyph_range.length()); - const SkScalar width = pos[glyph_range.end()].x() - - pos[glyph_range.start()].x(); - renderer.DrawDecorations(pos[glyph_range.start()].x(), y, - SkScalarCeilToInt(width), run->underline, - run->strike, run->diagonal_strike); - } + const Vector2d text_offset = line_offset + Vector2d(0, line.baseline); + int preceding_segment_widths = 0; + + for (size_t j = 0; j < line.segments.size(); ++j) { + const internal::LineSegment* segment = &line.segments[j]; + const int segment_width = segment->x_range.length(); + const internal::TextRun* run = runs_[segment->run]; + DCHECK(!segment->char_range.is_empty()); + DCHECK(run->range.Contains(segment->char_range)); + Range glyph_range = CharRangeToGlyphRange(*run, segment->char_range); + DCHECK(!glyph_range.is_empty()); + // Skip painting segments outside the display rect area. + if (!multiline()) { + const Rect segment_bounds(PointAtOffsetFromOrigin(line_offset) + + Vector2d(preceding_segment_widths, 0), + Size(segment_width, line.size.height())); + if (!display_rect().Intersects(segment_bounds)) { + preceding_segment_widths += segment_width; + continue; + } + } - DCHECK_EQ(glyph_x - x, run->width); - x = glyph_x; + // |pos| contains the positions of glyphs. An extra terminal |pos| entry + // is added to simplify width calculations. + int segment_x = preceding_segment_widths; + pos.resize(glyph_range.length() + 1); + for (size_t k = glyph_range.start(); k < glyph_range.end(); ++k) { + pos[k - glyph_range.start()].set( + SkIntToScalar(text_offset.x() + run->offsets[k].du + segment_x), + SkIntToScalar(text_offset.y() + run->offsets[k].dv)); + segment_x += run->advance_widths[k]; + } + pos.back().set(SkIntToScalar(text_offset.x() + segment_x), + SkIntToScalar(text_offset.y())); + + renderer.SetTextSize(run->font.GetFontSize()); + renderer.SetFontFamilyWithStyle(run->font.GetFontName(), run->font_style); + + for (BreakList<SkColor>::const_iterator it = + colors().GetBreak(segment->char_range.start()); + it != colors().breaks().end() && + it->first < segment->char_range.end(); + ++it) { + const Range intersection = + colors().GetRange(it).Intersect(segment->char_range); + const Range colored_glyphs = CharRangeToGlyphRange(*run, intersection); + DCHECK(glyph_range.Contains(colored_glyphs)); + DCHECK(!colored_glyphs.is_empty()); + const SkPoint& start_pos = + pos[colored_glyphs.start() - glyph_range.start()]; + const SkPoint& end_pos = + pos[colored_glyphs.end() - glyph_range.start()]; + + renderer.SetForegroundColor(it->second); + renderer.DrawPosText(&start_pos, &run->glyphs[colored_glyphs.start()], + colored_glyphs.length()); + renderer.DrawDecorations(start_pos.x(), text_offset.y(), + SkScalarCeilToInt(end_pos.x() - start_pos.x()), + run->underline, run->strike, + run->diagonal_strike); + } + + preceding_segment_widths += segment_width; + } } UndoCompositionAndSelectionStyles(); @@ -536,10 +853,8 @@ void RenderTextWin::DrawVisualText(Canvas* canvas) { void RenderTextWin::ItemizeLogicalText() { runs_.clear(); - // Make |string_size_|'s height and |common_baseline_| tall enough to draw - // often-used characters which are rendered with fonts in the font list. - string_size_ = Size(0, font_list().GetHeight()); - common_baseline_ = font_list().GetBaseline(); + string_width_ = 0; + multiline_string_size_ = Size(); // Set Uniscribe's base text direction. script_state_.uBidiLevel = @@ -669,8 +984,6 @@ void RenderTextWin::LayoutVisualText() { DCHECK(SUCCEEDED(hr)); } } - string_size_.set_height(ascent + descent); - common_baseline_ = ascent; // Build the array of bidirectional embedding levels. scoped_ptr<BYTE[]> levels(new BYTE[runs_.size()]); @@ -695,7 +1008,7 @@ void RenderTextWin::LayoutVisualText() { run->width = abc.abcA + abc.abcB + abc.abcC; preceding_run_widths += run->width; } - string_size_.set_width(preceding_run_widths); + string_width_ = preceding_run_widths; } void RenderTextWin::LayoutTextRun(internal::TextRun* run) { diff --git a/ui/gfx/render_text_win.h b/ui/gfx/render_text_win.h index 17e0671..b5e77b8 100644 --- a/ui/gfx/render_text_win.h +++ b/ui/gfx/render_text_win.h @@ -89,6 +89,8 @@ class RenderTextWin : public RenderText { private: FRIEND_TEST_ALL_PREFIXES(RenderTextTest, Win_BreakRunsByUnicodeBlocks); FRIEND_TEST_ALL_PREFIXES(RenderTextTest, Win_LogicalClusters); + FRIEND_TEST_ALL_PREFIXES(RenderTextTest, Multiline_MinWidth); + FRIEND_TEST_ALL_PREFIXES(RenderTextTest, Multiline_NormalWidth); void ItemizeLogicalText(); void LayoutVisualText(); @@ -123,11 +125,12 @@ class RenderTextWin : public RenderText { SCRIPT_STATE script_state_; ScopedVector<internal::TextRun> runs_; - Size string_size_; - // A common vertical baseline for all the text runs. This is computed as the - // largest baseline over all the runs' fonts. - int common_baseline_; + // Single line width of the layout text. + int string_width_; + + // Wrapped multiline size of the layout text. + Size multiline_string_size_; scoped_ptr<int[]> visual_to_logical_; scoped_ptr<int[]> logical_to_visual_; |