| // 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 |