blob: a2f1d586483856c4f1aef473eec1529262345c2f [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/views/webauthn/pin_textfield.h"
#include <algorithm>
#include <memory>
#include <string>
#include <utility>
#include "base/check_op.h"
#include "base/i18n/rtl.h"
#include "base/strings/strcat.h"
#include "cc/paint/paint_flags.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkPath.h"
#include "third_party/skia/include/core/SkRRect.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/base/ime/text_input_type.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/font_list.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/skia_conversions.h"
#include "ui/gfx/render_text.h"
#include "ui/gfx/text_constants.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/border.h"
#include "ui/views/style/typography.h"
#include "ui/views/style/typography_provider.h"
#include "ui/views/view.h"
namespace {
// Size specs of a pin cell.
constexpr int kCellWidth = 30;
constexpr int kCellHeight = 36;
constexpr int kCellSpacing = 8;
constexpr float kCellRadius = 8.0;
// Creates obscured render text for displaying a glyph in a specific pin cell.
std::unique_ptr<gfx::RenderText> CreatePinDigitRenderText(
const gfx::FontList& font_list) {
std::unique_ptr<gfx::RenderText> render_text =
gfx::RenderText::CreateRenderText();
render_text->SetCursorEnabled(false);
render_text->SetHorizontalAlignment(gfx::ALIGN_CENTER);
render_text->SetObscured(true);
render_text->SetFontList(font_list);
return render_text;
}
} // namespace
PinTextfield::PinTextfield(int pin_digits_amount)
: pin_digits_count_(pin_digits_amount) {
CHECK_GE(pin_digits_count_, 0);
SetCursorEnabled(false);
SetTextInputType(ui::TEXT_INPUT_TYPE_PASSWORD);
// Custom border handling is implemented in `OnPaint`.
SetBorder(views::CreateEmptyBorder(0));
const gfx::FontList& font_list = views::TypographyProvider::Get().GetFont(
views::style::CONTEXT_TEXTFIELD,
views::style::TextStyle::STYLE_BODY_1_BOLD);
for (int i = 0; i < pin_digits_count_; i++) {
render_texts_.push_back(CreatePinDigitRenderText(font_list));
}
UpdatePinAccessibleValue();
}
PinTextfield::~PinTextfield() = default;
bool PinTextfield::AppendDigit(std::u16string digit) {
if (disabled_) {
return false;
}
if (digits_typed_count_ >= pin_digits_count_) {
return false;
}
render_texts_[digits_typed_count_++]->SetText(std::move(digit));
SchedulePaint();
UpdateAccessibilityAfterPinChange();
return true;
}
bool PinTextfield::RemoveDigit() {
if (disabled_) {
return false;
}
if (digits_typed_count_ <= 0) {
return false;
}
render_texts_[--digits_typed_count_]->SetText(u"");
SchedulePaint();
UpdateAccessibilityAfterPinChange();
return true;
}
std::u16string PinTextfield::GetPin() {
std::u16string pin;
for (int i = 0; i < digits_typed_count_; i++) {
base::StrAppend(&pin, {render_texts_[i]->text()});
}
return pin;
}
void PinTextfield::SetPin(const std::u16string& pin) {
int pin_length = std::min(static_cast<int>(pin.length()), pin_digits_count_);
for (int i = 0; i < pin_length; i++) {
render_texts_[i]->SetText(std::u16string(1, pin[i]));
}
digits_typed_count_ = pin_length;
UpdatePinAccessibleValue();
SchedulePaint();
}
void PinTextfield::SetObscured(bool obscured) {
if (obscured_ == obscured) {
return;
}
obscured_ = obscured;
GetViewAccessibility().SetIsProtected(obscured);
for (int i = 0; i < pin_digits_count_; i++) {
render_texts_[i]->SetObscured(obscured);
}
UpdateAccessibilityAfterPinChange();
SchedulePaint();
}
void PinTextfield::SetDisabled(bool disabled) {
if (disabled_ == disabled) {
return;
}
disabled_ = disabled;
UpdatePinAccessibleValue();
SetTextInputType(disabled ? ui::TEXT_INPUT_TYPE_NONE
: ui::TEXT_INPUT_TYPE_PASSWORD);
UpdateTextColor();
SchedulePaint();
}
void PinTextfield::OnPaint(gfx::Canvas* canvas) {
View::OnPaintBackground(canvas);
cc::PaintFlags paint_flags;
paint_flags.setStyle(cc::PaintFlags::kStroke_Style);
paint_flags.setAntiAlias(true);
SkColor background_color = GetColorProvider()->GetColor(
disabled_ ? ui::kColorTextfieldBackgroundDisabled
: ui::kColorTextfieldBackground);
for (int i = 0; i < pin_digits_count_; i++) {
paint_flags.setColor(GetColorProvider()->GetColor(
disabled_ ? ui::kColorTextfieldOutlineDisabled
: (HasCellFocus(i) ? ui::kColorFocusableBorderFocused
: ui::kColorTextfieldOutline)));
float stroke_width = HasCellFocus(i) ? 2.f : 1.f;
paint_flags.setStrokeWidth(stroke_width);
// Drawing is adjusted in RTL so that the first cell is drawn rightmost.
int index_rtl_adjusted =
base::i18n::IsRTL() ? pin_digits_count_ - i - 1 : i;
gfx::Rect cell_rect(index_rtl_adjusted * (kCellWidth + kCellSpacing), 0,
kCellWidth, kCellHeight);
// Make sure background is not drawn outside of the rounded cell.
const SkPath path = SkPath::RRect(SkRRect::MakeRectXY(
gfx::RectToSkRect(cell_rect), kCellRadius, kCellRadius));
canvas->Save();
canvas->ClipPath(path, /*do_anti_alias=*/true);
// Draw cell background.
canvas->FillRect(cell_rect, background_color);
// Draw cell border.
gfx::RectF cell_rect_f(cell_rect);
cell_rect_f.Inset(stroke_width / 2.f);
canvas->DrawRoundRect(cell_rect_f, kCellRadius, paint_flags);
// Draw cell text.
render_texts_[i]->SetDisplayRect(cell_rect);
render_texts_[i]->Draw(canvas);
canvas->Restore();
}
}
gfx::Size PinTextfield::CalculatePreferredSize(
const views::SizeBounds& available_size) const {
return gfx::Size(
pin_digits_count_ * kCellWidth + (pin_digits_count_ - 1) * kCellSpacing,
kCellHeight);
}
void PinTextfield::OnThemeChanged() {
views::View::OnThemeChanged();
UpdateTextColor();
}
void PinTextfield::UpdateAccessibleTextSelection() {
// Pin textfield does not support selecting characters, set it to an empty
// selection at the end of the currently typed pin.
GetViewAccessibility().SetTextSelStart(digits_typed_count_);
GetViewAccessibility().SetTextSelEnd(digits_typed_count_);
}
bool PinTextfield::HasCellFocus(int cell) const {
int cell_with_focus = digits_typed_count_ == pin_digits_count_
? pin_digits_count_ - 1
: digits_typed_count_;
return HasFocus() && cell == cell_with_focus;
}
void PinTextfield::UpdateAccessibilityAfterPinChange() {
UpdateAccessibleTextSelection();
UpdatePinAccessibleValue();
// Don't announce the selected text (last typed digit) in `obscured_` mode.
if (!obscured_) {
NotifyAccessibilityEventDeprecated(ax::mojom::Event::kTextSelectionChanged,
/*send_native_event=*/true);
}
}
void PinTextfield::UpdatePinAccessibleValue() {
std::u16string pin = GetPin();
GetViewAccessibility().SetValue(
(obscured_ || disabled_)
? std::u16string(pin.size(),
gfx::RenderText::kPasswordReplacementChar)
: pin);
}
void PinTextfield::UpdateTextColor() {
if (!GetWidget()) {
return;
}
int text_style =
disabled_ ? views::style::STYLE_DISABLED : views::style::STYLE_PRIMARY;
SkColor text_color =
GetColorProvider()->GetColor(views::TypographyProvider::Get().GetColorId(
views::style::CONTEXT_TEXTFIELD, text_style));
for (int i = 0; i < pin_digits_count_; i++) {
render_texts_[i]->SetColor(text_color);
}
}
BEGIN_METADATA(PinTextfield)
END_METADATA