blob: 615efd30f82c13e99675c87821c2427d9986a826 [file] [log] [blame]
// 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