| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/vr/elements/text.h" |
| |
| #include "cc/paint/skia_paint_canvas.h" |
| #include "chrome/browser/vr/elements/render_text_wrapper.h" |
| #include "chrome/browser/vr/elements/ui_texture.h" |
| #include "third_party/icu/source/common/unicode/uscript.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/font_list.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/gfx/geometry/safe_integer_conversions.h" |
| #include "ui/gfx/geometry/vector2d.h" |
| #include "ui/gfx/render_text.h" |
| #include "ui/gfx/shadow_value.h" |
| #include "ui/gfx/text_elider.h" |
| |
| namespace vr { |
| |
| namespace { |
| |
| constexpr float kCursorWidthRatio = 0.07f; |
| constexpr int kTextPixelPerDmm = 1100; |
| constexpr float kTextShadowScaleFactor = 1000.0f; |
| constexpr char kDefaultFontFamily[] = "sans-serif"; |
| |
| int DmmToPixel(float dmm) { |
| return static_cast<int>(dmm * kTextPixelPerDmm); |
| } |
| |
| float PixelToDmm(int pixel) { |
| return static_cast<float>(pixel) / kTextPixelPerDmm; |
| } |
| |
| bool IsFixedWidthLayout(TextLayoutMode mode) { |
| return mode == kSingleLineFixedWidth || mode == kMultiLineFixedWidth; |
| } |
| |
| void UpdateRenderText(gfx::RenderText* render_text, |
| const base::string16& text, |
| const gfx::FontList& font_list, |
| SkColor color, |
| TextAlignment text_alignment, |
| bool shadows_enabled, |
| SkColor shadow_color, |
| float shadow_size) { |
| // Disable the cursor to avoid reserving width for a trailing caret. |
| render_text->SetCursorEnabled(false); |
| |
| // Subpixel rendering is counterproductive when drawing VR textures. |
| render_text->set_subpixel_rendering_suppressed(true); |
| |
| render_text->SetText(text); |
| render_text->SetFontList(font_list); |
| render_text->SetColor(color); |
| if (shadows_enabled) { |
| render_text->set_shadows( |
| {gfx::ShadowValue({0, 0}, shadow_size, shadow_color)}); |
| } else { |
| render_text->set_shadows({}); |
| } |
| |
| switch (text_alignment) { |
| case kTextAlignmentNone: |
| break; |
| case kTextAlignmentLeft: |
| render_text->SetHorizontalAlignment(gfx::ALIGN_LEFT); |
| break; |
| case kTextAlignmentRight: |
| render_text->SetHorizontalAlignment(gfx::ALIGN_RIGHT); |
| break; |
| case kTextAlignmentCenter: |
| render_text->SetHorizontalAlignment(gfx::ALIGN_CENTER); |
| break; |
| } |
| |
| const int font_style = font_list.GetFontStyle(); |
| render_text->SetStyle(gfx::TEXT_STYLE_ITALIC, |
| (font_style & gfx::Font::ITALIC) != 0); |
| render_text->SetStyle(gfx::TEXT_STYLE_UNDERLINE, |
| (font_style & gfx::Font::UNDERLINE) != 0); |
| render_text->SetWeight(font_list.GetFontWeight()); |
| } |
| |
| } // namespace |
| |
| TextFormattingAttribute::TextFormattingAttribute(SkColor color, |
| const gfx::Range& range) |
| : type_(COLOR), range_(range), color_(color) {} |
| |
| TextFormattingAttribute::TextFormattingAttribute(gfx::Font::Weight weight, |
| const gfx::Range& range) |
| : type_(WEIGHT), range_(range), weight_(weight) {} |
| |
| TextFormattingAttribute::TextFormattingAttribute( |
| gfx::DirectionalityMode directionality) |
| : type_(DIRECTIONALITY), directionality_(directionality) {} |
| |
| bool TextFormattingAttribute::operator==( |
| const TextFormattingAttribute& other) const { |
| if (type_ != other.type_ || range_ != other.range_) |
| return false; |
| switch (type_) { |
| case COLOR: |
| return color_ == other.color_; |
| case WEIGHT: |
| return weight_ == other.weight_; |
| case DIRECTIONALITY: |
| return directionality_ == other.directionality_; |
| default: |
| NOTREACHED(); |
| return false; |
| } |
| } |
| |
| bool TextFormattingAttribute::operator!=( |
| const TextFormattingAttribute& other) const { |
| return !(*this == other); |
| } |
| |
| void TextFormattingAttribute::Apply(RenderTextWrapper* render_text) const { |
| switch (type_) { |
| case COLOR: { |
| if (range_.IsValid()) { |
| render_text->ApplyColor(color_, range_); |
| } else { |
| render_text->SetColor(color_); |
| } |
| break; |
| } |
| case WEIGHT: |
| if (range_.IsValid()) { |
| render_text->ApplyWeight(weight_, range_); |
| } else { |
| render_text->SetWeight(weight_); |
| } |
| break; |
| case DIRECTIONALITY: |
| render_text->SetDirectionalityMode(directionality_); |
| break; |
| default: |
| NOTREACHED(); |
| } |
| } |
| |
| class TextTexture : public UiTexture { |
| public: |
| explicit TextTexture(Text* element) : element_(element) {} |
| |
| ~TextTexture() override {} |
| |
| void SetFontHeightInDmm(float font_height_dmms) { |
| SetAndDirty(&font_height_dmms_, font_height_dmms); |
| } |
| |
| void SetText(const base::string16& text) { SetAndDirty(&text_, text); } |
| |
| void SetColor(SkColor color) { SetAndDirty(&color_, color); } |
| |
| void SetSelectionColors(const TextSelectionColors& colors) { |
| SetAndDirty(&selection_colors_, colors); |
| } |
| |
| void SetFormatting(const TextFormatting& formatting) { |
| SetAndDirty(&formatting_, formatting); |
| } |
| |
| void SetAlignment(TextAlignment alignment) { |
| SetAndDirty(&alignment_, alignment); |
| } |
| |
| void SetLayoutMode(TextLayoutMode mode) { |
| SetAndDirty(&text_layout_mode_, mode); |
| } |
| |
| void SetCursorEnabled(bool enabled) { |
| SetAndDirty(&cursor_enabled_, enabled); |
| } |
| |
| void SetSelectionIndices(int start, int end) { |
| SetAndDirty(&selection_start_, start); |
| SetAndDirty(&selection_end_, end); |
| } |
| |
| void SetShadowsEnabled(bool enabled) { |
| SetAndDirty(&shadows_enabled_, enabled); |
| } |
| |
| void SetTextWidth(float width) { SetAndDirty(&text_width_, width); } |
| |
| gfx::Rect get_cursor_bounds() { return cursor_bounds_; } |
| |
| // This method does all text preparation for the element other than drawing to |
| // the texture. This allows for deeper unit testing of the Text element |
| // without having to mock canvases and simulate frame rendering. The state of |
| // the texture is modified here. |
| gfx::Size LayOutText(); |
| |
| const std::vector<std::unique_ptr<gfx::RenderText>>& lines() const { |
| return lines_; |
| } |
| |
| void SetOnRenderTextCreated( |
| base::RepeatingCallback<void(gfx::RenderText*)> callback) { |
| render_text_created_callback_ = callback; |
| } |
| |
| void SetOnRenderTextRendered( |
| base::RepeatingCallback<void(const gfx::RenderText&, SkCanvas* canvas)> |
| callback) { |
| render_text_rendered_callback_ = callback; |
| } |
| |
| void set_unsupported_code_points_for_test(bool unsupported) { |
| unsupported_code_point_for_test_ = unsupported; |
| } |
| |
| private: |
| void Draw(SkCanvas* sk_canvas, const gfx::Size& texture_size) override; |
| |
| void PrepareDrawStringRect(const base::string16& text, |
| const gfx::FontList& font_list, |
| gfx::Rect* bounds, |
| const TextRenderParameters& parameters); |
| void PrepareDrawWrapText(const base::string16& text, |
| const gfx::FontList& font_list, |
| gfx::Rect* bounds, |
| const TextRenderParameters& parameters); |
| void PrepareDrawSingleLineText(const base::string16& text, |
| const gfx::FontList& font_list, |
| gfx::Rect* bounds, |
| const TextRenderParameters& parameters); |
| |
| gfx::SizeF size_; |
| gfx::Vector2d texture_offset_; |
| base::string16 text_; |
| float font_height_dmms_ = 0; |
| float text_width_ = 0; |
| TextAlignment alignment_ = kTextAlignmentCenter; |
| TextLayoutMode text_layout_mode_ = kMultiLineFixedWidth; |
| SkColor color_ = SK_ColorBLACK; |
| TextSelectionColors selection_colors_; |
| TextFormatting formatting_; |
| bool cursor_enabled_ = false; |
| int selection_start_ = 0; |
| int selection_end_ = 0; |
| gfx::Rect cursor_bounds_; |
| bool shadows_enabled_ = false; |
| std::vector<std::unique_ptr<gfx::RenderText>> lines_; |
| Text* element_ = nullptr; |
| |
| base::RepeatingCallback<void(gfx::RenderText*)> render_text_created_callback_; |
| base::RepeatingCallback<void(const gfx::RenderText&, SkCanvas*)> |
| render_text_rendered_callback_; |
| |
| bool unsupported_code_point_for_test_ = false; |
| |
| DISALLOW_COPY_AND_ASSIGN(TextTexture); |
| }; |
| |
| Text::Text(float font_height_dmms) |
| : TexturedElement(), texture_(std::make_unique<TextTexture>(this)) { |
| texture_->SetFontHeightInDmm(font_height_dmms); |
| } |
| |
| Text::~Text() {} |
| |
| void Text::SetFontHeightInDmm(float font_height_dmms) { |
| texture_->SetFontHeightInDmm(font_height_dmms); |
| } |
| |
| void Text::SetText(const base::string16& text) { |
| texture_->SetText(text); |
| } |
| |
| void Text::SetFieldWidth(float width) { |
| field_width_ = width; |
| texture_->SetTextWidth(width); |
| } |
| |
| void Text::SetColor(SkColor color) { |
| texture_->SetColor(color); |
| } |
| |
| void Text::SetSelectionColors(const TextSelectionColors& colors) { |
| texture_->SetSelectionColors(colors); |
| } |
| |
| void Text::SetFormatting(const TextFormatting& formatting) { |
| texture_->SetFormatting(formatting); |
| } |
| |
| void Text::SetAlignment(TextAlignment alignment) { |
| texture_->SetAlignment(alignment); |
| } |
| |
| void Text::SetLayoutMode(TextLayoutMode mode) { |
| text_layout_mode_ = mode; |
| texture_->SetLayoutMode(mode); |
| } |
| |
| void Text::SetCursorEnabled(bool enabled) { |
| texture_->SetCursorEnabled(enabled); |
| } |
| |
| void Text::SetSelectionIndices(int start, int end) { |
| texture_->SetSelectionIndices(start, end); |
| } |
| |
| gfx::Rect Text::GetRawCursorBounds() const { |
| return texture_->get_cursor_bounds(); |
| } |
| |
| gfx::RectF Text::GetCursorBounds() const { |
| // Note that gfx:: cursor bounds always indicate a one-pixel width, so we |
| // override the width here to be a percentage of height for the sake of |
| // arbitrary texture sizes. |
| gfx::Rect bounds = texture_->get_cursor_bounds(); |
| float scale = size().width() / text_texture_size_.width(); |
| return gfx::RectF( |
| bounds.CenterPoint().x() * scale, bounds.CenterPoint().y() * scale, |
| bounds.height() * scale * kCursorWidthRatio, bounds.height() * scale); |
| } |
| |
| int Text::GetCursorPositionFromPoint(const gfx::PointF& point) const { |
| DCHECK_EQ(texture_->lines().size(), 1u); |
| gfx::Point pixel_position(point.x() * text_texture_size_.width(), |
| point.y() * text_texture_size_.height()); |
| return texture_->lines() |
| .front() |
| ->FindCursorPosition(pixel_position) |
| .caret_pos(); |
| } |
| |
| void Text::SetShadowsEnabled(bool enabled) { |
| texture_->SetShadowsEnabled(enabled); |
| } |
| |
| const std::vector<std::unique_ptr<gfx::RenderText>>& Text::LinesForTest() { |
| return texture_->lines(); |
| } |
| |
| void Text::SetUnsupportedCodePointsForTest(bool unsupported) { |
| texture_->set_unsupported_code_points_for_test(unsupported); |
| } |
| |
| void Text::SetOnRenderTextCreated( |
| base::RepeatingCallback<void(gfx::RenderText*)> callback) { |
| texture_->SetOnRenderTextCreated(callback); |
| } |
| |
| void Text::SetOnRenderTextRendered( |
| base::RepeatingCallback<void(const gfx::RenderText&, SkCanvas* canvas)> |
| callback) { |
| texture_->SetOnRenderTextRendered(callback); |
| } |
| |
| float Text::MetersToPixels(float meters) { |
| return DmmToPixel(meters); |
| } |
| |
| UiTexture* Text::GetTexture() const { |
| return texture_.get(); |
| } |
| |
| bool Text::TextureDependsOnMeasurement() const { |
| return true; |
| } |
| |
| gfx::Size Text::MeasureTextureSize() { |
| text_texture_size_ = texture_->LayOutText(); |
| |
| // Adjust the actual size of the element to match the texture. |
| float width = IsFixedWidthLayout(text_layout_mode_) |
| ? field_width_ |
| : PixelToDmm(text_texture_size_.width()); |
| TexturedElement::SetSize(width, PixelToDmm(text_texture_size_.height())); |
| |
| return text_texture_size_; |
| } |
| |
| gfx::Size TextTexture::LayOutText() { |
| int pixel_font_height = DmmToPixel(font_height_dmms_); |
| gfx::Rect text_bounds; |
| if (IsFixedWidthLayout(text_layout_mode_)) { |
| DCHECK(text_width_ > 0.f) << element_->DebugName(); |
| text_bounds.set_width(DmmToPixel(text_width_)); |
| } |
| |
| gfx::FontList fonts = |
| gfx::FontList(gfx::Font(kDefaultFontFamily, pixel_font_height)); |
| |
| TextRenderParameters parameters; |
| parameters.color = color_; |
| parameters.text_alignment = alignment_; |
| parameters.wrapping_behavior = text_layout_mode_ == kMultiLineFixedWidth |
| ? kWrappingBehaviorWrap |
| : kWrappingBehaviorNoWrap; |
| parameters.cursor_enabled = cursor_enabled_; |
| parameters.cursor_position = selection_end_; |
| parameters.shadows_enabled = shadows_enabled_; |
| parameters.shadow_size = kTextShadowScaleFactor * font_height_dmms_; |
| |
| PrepareDrawStringRect(text_, fonts, &text_bounds, parameters); |
| |
| if (cursor_enabled_) { |
| DCHECK_EQ(lines_.size(), 1u); |
| gfx::RenderText* render_text = lines_.front().get(); |
| |
| if (selection_start_ != selection_end_) { |
| render_text->set_focused(true); |
| gfx::Range range(selection_start_, selection_end_); |
| render_text->SetSelection(gfx::SelectionModel( |
| range, gfx::LogicalCursorDirection::CURSOR_FORWARD)); |
| render_text->set_selection_background_focused_color( |
| selection_colors_.background); |
| render_text->set_selection_color(selection_colors_.foreground); |
| } |
| |
| cursor_bounds_ = render_text->GetUpdatedCursorBounds(); |
| } |
| |
| if (!formatting_.empty()) { |
| DCHECK_EQ(parameters.wrapping_behavior, kWrappingBehaviorNoWrap); |
| DCHECK_EQ(lines_.size(), 1u); |
| RenderTextWrapper render_text(lines_.front().get()); |
| for (const auto& attribute : formatting_) { |
| attribute.Apply(&render_text); |
| } |
| } |
| |
| if (render_text_created_callback_) { |
| DCHECK_EQ(lines_.size(), 1u); |
| render_text_created_callback_.Run(lines_.front().get()); |
| } |
| |
| // Note, there is no padding here whatsoever. |
| if (parameters.shadows_enabled) { |
| texture_offset_ = gfx::Vector2d(gfx::ToFlooredInt(parameters.shadow_size), |
| gfx::ToFlooredInt(parameters.shadow_size)); |
| } |
| |
| set_measured(); |
| |
| return text_bounds.size(); |
| } |
| |
| void TextTexture::Draw(SkCanvas* sk_canvas, const gfx::Size& texture_size) { |
| cc::SkiaPaintCanvas paint_canvas(sk_canvas); |
| gfx::Canvas gfx_canvas(&paint_canvas, 1.0f); |
| gfx::Canvas* canvas = &gfx_canvas; |
| canvas->Translate(texture_offset_); |
| |
| for (auto& render_text : lines_) |
| render_text->Draw(canvas); |
| |
| if (render_text_rendered_callback_) { |
| DCHECK_EQ(lines_.size(), 1u); |
| render_text_rendered_callback_.Run(*lines_.front().get(), sk_canvas); |
| } |
| } |
| |
| void TextTexture::PrepareDrawStringRect( |
| const base::string16& text, |
| const gfx::FontList& font_list, |
| gfx::Rect* bounds, |
| const TextRenderParameters& parameters) { |
| DCHECK(bounds); |
| |
| if (parameters.wrapping_behavior == kWrappingBehaviorWrap) |
| PrepareDrawWrapText(text, font_list, bounds, parameters); |
| else |
| PrepareDrawSingleLineText(text, font_list, bounds, parameters); |
| |
| if (parameters.shadows_enabled) { |
| bounds->Inset(-parameters.shadow_size, -parameters.shadow_size); |
| bounds->Offset(parameters.shadow_size, parameters.shadow_size); |
| } |
| } |
| |
| void TextTexture::PrepareDrawWrapText(const base::string16& text, |
| const gfx::FontList& font_list, |
| gfx::Rect* bounds, |
| const TextRenderParameters& parameters) { |
| lines_.clear(); |
| DCHECK(!parameters.cursor_enabled); |
| |
| gfx::Rect rect(*bounds); |
| std::vector<base::string16> strings; |
| gfx::ElideRectangleText(text, font_list, bounds->width(), |
| bounds->height() ? bounds->height() : INT_MAX, |
| gfx::WRAP_LONG_WORDS, &strings); |
| |
| int height = 0; |
| int line_height = 0; |
| for (size_t i = 0; i < strings.size(); i++) { |
| auto render_text = gfx::RenderText::CreateHarfBuzzInstance(); |
| UpdateRenderText(render_text.get(), strings[i], font_list, parameters.color, |
| parameters.text_alignment, parameters.shadows_enabled, |
| parameters.shadow_color, parameters.shadow_size); |
| |
| if (i == 0) { |
| // Measure line and center text vertically. |
| line_height = render_text->GetStringSize().height(); |
| rect.set_height(line_height); |
| if (bounds->height()) { |
| const int text_height = strings.size() * line_height; |
| rect += gfx::Vector2d(0, (bounds->height() - text_height) / 2); |
| } |
| } |
| |
| render_text->SetDisplayRect(rect); |
| height += line_height; |
| rect += gfx::Vector2d(0, line_height); |
| lines_.push_back(std::move(render_text)); |
| } |
| |
| // Set calculated height. |
| if (bounds->height() == 0) |
| bounds->set_height(height); |
| } |
| |
| void TextTexture::PrepareDrawSingleLineText( |
| const base::string16& text, |
| const gfx::FontList& font_list, |
| gfx::Rect* bounds, |
| const TextRenderParameters& parameters) { |
| if (lines_.size() != 1) { |
| lines_.clear(); |
| lines_.push_back(gfx::RenderText::CreateHarfBuzzInstance()); |
| } |
| |
| auto* render_text = lines_.front().get(); |
| UpdateRenderText(render_text, text, font_list, parameters.color, |
| parameters.text_alignment, parameters.shadows_enabled, |
| parameters.shadow_color, parameters.shadow_size); |
| if (bounds->width() != 0 && !parameters.cursor_enabled) |
| render_text->SetElideBehavior(gfx::TRUNCATE); |
| if (parameters.cursor_enabled) { |
| render_text->SetCursorEnabled(true); |
| render_text->SetCursorPosition(parameters.cursor_position); |
| } |
| if (bounds->width() == 0) |
| bounds->set_width(render_text->GetStringSize().width()); |
| if (bounds->height() == 0) |
| bounds->set_height(render_text->GetStringSize().height()); |
| |
| render_text->SetDisplayRect(*bounds); |
| } |
| |
| } // namespace vr |