chromium/ash/auth/views/auth_input_row_view.cc

// 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 "ash/auth/views/auth_input_row_view.h"

#include "ash/accessibility/accessibility_controller.h"
#include "ash/auth/views/auth_textfield.h"
#include "ash/login/ui/arrow_button_view.h"
#include "ash/login/ui/hover_notifier.h"
#include "ash/login/ui/non_accessible_view.h"
#include "ash/public/cpp/login_types.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/color_util.h"
#include "ash/style/icon_button.h"
#include "ash/style/style_util.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/utf_string_conversions.h"
#include "chromeos/constants/chromeos_features.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/image_model.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/compositor/layer_animation_observer.h"
#include "ui/events/event_constants.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/gfx/text_constants.h"
#include "ui/resources/grit/ui_resources.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/focus_ring.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/highlight_border.h"
#include "ui/views/layout/box_layout.h"

namespace ash {
namespace {

// Spacing between the icons (caps lock, display password) and the
// borders of the input row. Note that if there is no icon, the padding will
// appear to be 8dp since the input textfield has a 2dp margin.
constexpr const int kIntenalHorizontalPaddingInputRowDp = 6;

// Spacing between the input row and the submit button.
constexpr int kSpacingBetweenInputRowAndSubmitButtonDp = 5;

// Size (width/height) of the submit button.
constexpr int kSubmitButtonContentSizeDp = 32;

// Size (width/height) of the submit button, border included.
constexpr int kSubmitButtonSizeDp = kSubmitButtonContentSizeDp + 6;

// Left padding of the input row  view allowing the view to have its center
// aligned with the one of the user pod.
constexpr int kLeftPaddingInputRowView =
    kSubmitButtonSizeDp + kSpacingBetweenInputRowAndSubmitButtonDp;

// Width of the input row, placed at the center of the input row view
// (which also contains the submit button).
constexpr int kInputRowWidthDp = 204;

// Total width of the input row view (left margin + input row + spacing +
// submit button).
constexpr int kInputRowTotalWidthDp = kLeftPaddingInputRowView +
                                      kInputRowWidthDp + kSubmitButtonSizeDp +
                                      kSpacingBetweenInputRowAndSubmitButtonDp;

// Height of the input row view
constexpr int kInputRowHeightDp = 32;

// Size (width/height) of the different icons belonging to the input row
// (the display text icon and the caps lock icon).
constexpr const int kIconSizeDp = 20;

// The input textfield has an external margin because we want these specific
// visual results following in these different cases:
// icon-textfield-icon: 6dp - icon - 8dp - textfield - 8dp - icon - 6dp
// textfield-icon:      8dp - textfield - 8dp - icon - 6dp
// icon-textfield:      6dp - icon - 8dp - textfield - 8dp
// textfield:           8dp - textfield - 8dp
// This translates by having a 6dp spacing between children of the input
// row, having a 6dp padding for the input row and having a 2dp margin for
// the input textfield.
constexpr const int kInputTextfieldMarginDp = 2;

// The corner radius of the input row.
constexpr const int kInputRowCornerRadiusDp = 8;

// The inset of the input row and it's focus ring.
constexpr const int kInputRowFocusRingInsetDp = 2;

// The focus ring corner radius.
constexpr const int kInputRowFocusRingRadiusDp =
    kInputRowCornerRadiusDp + kInputRowFocusRingInsetDp;

// Horizontal spacing between the end of the input textfield and the display
// text button. Note that the input textfield has a 2dp margin so the
// ending result will be 8dp.
constexpr const int kHorizontalSpacingBetweenIconsAndTextfieldDp = 6;

const ui::ImageModel kCapslockIconHighlighted =
    ui::ImageModel::FromVectorIcon(kLockScreenCapsLockIcon,
                                   cros_tokens::kCrosSysOnSurface);

const ui::ImageModel kCapslockIconBlurred =
    ui::ImageModel::FromVectorIcon(kLockScreenCapsLockIcon,
                                   cros_tokens::kCrosSysDisabled);

}  // namespace

AuthInputRowView::TestApi::TestApi(AuthInputRowView* view) : view_(view) {}

AuthInputRowView::TestApi::~TestApi() = default;

raw_ptr<AuthTextfield> AuthInputRowView::TestApi::GetTextfield() const {
  return view_->textfield_;
}

raw_ptr<views::ToggleImageButton>
AuthInputRowView::TestApi::GetDisplayTextButton() const {
  return view_->display_text_button_;
}

raw_ptr<IconButton> AuthInputRowView::TestApi::GetSubmitButton() const {
  return view_->submit_button_;
}

raw_ptr<views::ImageView> AuthInputRowView::TestApi::GetCapsLockIcon() const {
  return view_->capslock_icon_;
}

raw_ptr<AuthInputRowView> AuthInputRowView::TestApi::GetView() const {
  return view_;
}

AuthInputRowView::AuthInputRowView(AuthType auth_type) : auth_type_(auth_type) {
  input_methods_observer_.Observe(Shell::Get()->ime_controller());

  ConfigureRootLayout();
  CreateAndConfigureInputRow();
  CreateAndConfigureCapslockIcon();
  CreateAndConfigureTextfieldContainer();
  CreateFocusRingForInputRow();
  CreateAndConfigureDisplayTextButton();
  CreateAndConfigureSubmitButton();
  SetDisplayTextButtonVisible(true);
}

AuthInputRowView::~AuthInputRowView() = default;

void AuthInputRowView::ConfigureRootLayout() {
  // Contains the input row layout on the left and the submit button on the
  // right.
  SetLayoutManager(std::make_unique<views::BoxLayout>(
                       views::BoxLayout::Orientation::kHorizontal,
                       gfx::Insets::TLBR(0, kLeftPaddingInputRowView, 0, 0),
                       kSpacingBetweenInputRowAndSubmitButtonDp))
      ->set_main_axis_alignment(views::BoxLayout::MainAxisAlignment::kEnd);
}

void AuthInputRowView::CreateAndConfigureInputRow() {
  auto* input_row_container =
      AddChildView(std::make_unique<NonAccessibleView>());
  // The input row should have the same visible height than the submit
  // button. Since the login password view has the same height than the submit
  // button – border included – we need to remove its border.
  auto* input_row_container_layout =
      input_row_container->SetLayoutManager(std::make_unique<views::BoxLayout>(
          views::BoxLayout::Orientation::kVertical));
  input_row_container_layout->set_main_axis_alignment(
      views::BoxLayout::MainAxisAlignment::kCenter);

  input_row_ =
      input_row_container->AddChildView(std::make_unique<views::View>());
  input_row_->SetBackground(views::CreateThemedRoundedRectBackground(
      cros_tokens::kCrosSysSystemOnBase, kInputRowCornerRadiusDp));

  input_row_->SetBorder(std::make_unique<views::HighlightBorder>(
      kInputRowCornerRadiusDp,
      views::HighlightBorder::Type::kHighlightBorderNoShadow));

  auto layout = std::make_unique<views::BoxLayout>(
      views::BoxLayout::Orientation::kHorizontal,
      gfx::Insets::VH(0, kIntenalHorizontalPaddingInputRowDp),
      kHorizontalSpacingBetweenIconsAndTextfieldDp);
  layout->set_main_axis_alignment(views::BoxLayout::MainAxisAlignment::kCenter);
  layout->set_cross_axis_alignment(
      views::BoxLayout::CrossAxisAlignment::kCenter);
  input_row_layout_ = input_row_->SetLayoutManager(std::move(layout));

  // Make the input row fill the view.
  input_row_container_layout->SetFlexForView(input_row_, 1);
}

void AuthInputRowView::CreateAndConfigureCapslockIcon() {
  capslock_icon_ =
      input_row_->AddChildView(std::make_unique<views::ImageView>());
  capslock_icon_->SetTooltipText(
      l10n_util::GetStringUTF16(IDS_ASH_LOGIN_CAPS_LOCK_ACCESSIBLE_NAME));
  capslock_icon_->SetVisible(false);
}

void AuthInputRowView::CreateAndConfigureTextfieldContainer() {
  auto* textfield_container =
      input_row_->AddChildView(std::make_unique<NonAccessibleView>());
  textfield_container->SetLayoutManager(std::make_unique<views::BoxLayout>(
      views::BoxLayout::Orientation::kHorizontal,
      gfx::Insets::VH(0, kInputTextfieldMarginDp)));

  // Input textfield. We control the textfield size by sizing the parent
  // view, as the textfield will expand to fill it.
  textfield_ =
      textfield_container->AddChildView(std::make_unique<AuthTextfield>(
          auth_type_ == AuthType::kPassword ? AuthTextfield::AuthType::kPassword
                                            : AuthTextfield::AuthType::kPin));
  textfield_->AddObserver(this);

  input_row_layout_->SetFlexForView(textfield_container, 1);
}

void AuthInputRowView::CreateFocusRingForInputRow() {
  CHECK_NE(textfield_, nullptr);

  StyleUtil::SetUpFocusRingForView(input_row_);
  views::FocusRing::Get(input_row_)
      ->SetPathGenerator(
          std::make_unique<views::RoundRectHighlightPathGenerator>(
              -gfx::Insets::VH(kInputRowFocusRingInsetDp,
                               kInputRowFocusRingInsetDp),
              kInputRowFocusRingRadiusDp));
  views::FocusRing::Get(input_row_)
      ->SetHasFocusPredicate(base::BindRepeating(
          [](const AuthTextfield* textfield, const views::View* view) {
            return textfield->IsActive();
          },
          textfield_));
}

void AuthInputRowView::CreateAndConfigureSubmitButton() {
  IconButton::Builder builder;
  builder.SetType(IconButton::Type::kMedium)
      .SetAccessibleNameId(IDS_ASH_LOGIN_SUBMIT_BUTTON_ACCESSIBLE_NAME)
      .SetCallback(base::BindRepeating(&AuthInputRowView::OnSubmit,
                                       weak_ptr_factory_.GetWeakPtr()))
      .SetTogglable(true)
      .SetEnabled(false)
      .SetBorder(true)
      .SetBackgroundColor(cros_tokens::kCrosSysDisabledContainer)
      .SetVectorIcon(&kLockScreenArrowIcon);

  submit_button_ = AddChildView(builder.Build());

  submit_button_->SetTooltipText(
      l10n_util::GetStringUTF16(IDS_ASH_LOGIN_SUBMIT_BUTTON_ACCESSIBLE_NAME));
}

void AuthInputRowView::CreateAndConfigureDisplayTextButton() {
  display_text_button_ =
      input_row_->AddChildView(std::make_unique<views::ToggleImageButton>(
          base::BindRepeating(&AuthInputRowView::ToggleTextDisplayingState,
                              base::Unretained(this))));

  display_text_button_->SetTooltipText(l10n_util::GetStringUTF16(
      IDS_ASH_LOGIN_DISPLAY_PASSWORD_BUTTON_ACCESSIBLE_NAME_SHOW));
  display_text_button_->SetToggledTooltipText(l10n_util::GetStringUTF16(
      IDS_ASH_LOGIN_DISPLAY_PASSWORD_BUTTON_ACCESSIBLE_NAME_HIDE));
  display_text_button_->SetFocusBehavior(FocusBehavior::ALWAYS);
  display_text_button_->SetInstallFocusRingOnFocus(true);
  views::FocusRing::Get(display_text_button_)
      ->SetColorId(ui::kColorAshFocusRing);

  const ui::ImageModel invisible_icon = ui::ImageModel::FromVectorIcon(
      kLockScreenPasswordInvisibleIcon, cros_tokens::kCrosSysOnSurface,
      kIconSizeDp);
  const ui::ImageModel visible_icon = ui::ImageModel::FromVectorIcon(
      kLockScreenPasswordVisibleIcon, cros_tokens::kCrosSysOnSurface,
      kIconSizeDp);
  const ui::ImageModel visible_icon_disabled = ui::ImageModel::FromVectorIcon(
      kLockScreenPasswordVisibleIcon, cros_tokens::kCrosSysDisabled,
      kIconSizeDp);
  display_text_button_->SetImageModel(views::Button::STATE_NORMAL,
                                      visible_icon);
  display_text_button_->SetImageModel(views::Button::STATE_DISABLED,
                                      visible_icon_disabled);
  display_text_button_->SetToggledImageModel(views::Button::STATE_NORMAL,
                                             invisible_icon);

  display_text_button_->SetEnabled(false);
}

void AuthInputRowView::OnTextfieldBlur() {
  views::FocusRing::Get(input_row_)->SchedulePaint();
  SetCapsLockHighlighted(false);
  for (auto& observer : observers_) {
    observer.OnTextfieldBlur();
  }
}

void AuthInputRowView::OnTextfieldFocus() {
  views::FocusRing::Get(input_row_)->SchedulePaint();
  SetCapsLockHighlighted(true);
  for (auto& observer : observers_) {
    observer.OnTextfieldFocus();
  }
}

void AuthInputRowView::OnContentsChanged(const std::u16string& new_contents) {
  bool enable_buttons = !textfield_->GetReadOnly() && !new_contents.empty();
  if (new_contents.empty() && textfield_->IsTextVisible()) {
    ToggleTextDisplayingState();
  }

  if (!enable_buttons) {
    // If the submit or eye icon had the focus we should pass the focus to the
    // textfield_.
    if (submit_button_->HasFocus() || display_text_button_->HasFocus()) {
      RequestFocus();
    }
  }
  submit_button_->SetEnabled(enable_buttons);
  display_text_button_->SetEnabled(enable_buttons);
  for (auto& observer : observers_) {
    observer.OnContentsChanged(new_contents);
  }
}

void AuthInputRowView::OnTextVisibleChanged(bool visible) {
  display_text_button_->SetToggled(visible);
  for (auto& observer : observers_) {
    observer.OnTextVisibleChanged(visible);
  }
}

void AuthInputRowView::OnSubmit() {
  Submit();
}

void AuthInputRowView::OnEscape() {
  Escape();
}

void AuthInputRowView::SetDisplayTextButtonVisible(bool visible) {
  display_text_button_->SetVisible(visible);
  // Only start the timer if the display password button is enabled.
  if (visible) {
    textfield_->ApplyTimerLogic();
  } else {
    textfield_->ResetTimerLogic();
  }
}

gfx::Size AuthInputRowView::CalculatePreferredSize(
    const views::SizeBounds& available_size) const {
  return gfx::Size({kInputRowTotalWidthDp, kInputRowHeightDp});
}

void AuthInputRowView::RequestFocus() {
  textfield_->RequestFocus();
}

bool AuthInputRowView::OnKeyPressed(const ui::KeyEvent& event) {
  if (event.key_code() == ui::KeyboardCode::VKEY_ESCAPE) {
    Escape();
    return true;
  }

  return false;
}

void AuthInputRowView::ToggleTextDisplayingState() {
  textfield_->SetTextVisible(!textfield_->IsTextVisible());
}

void AuthInputRowView::OnCapsLockChanged(bool enabled) {
  capslock_icon_->SetVisible(enabled);
  for (auto& observer : observers_) {
    observer.OnCapsLockStateChanged(enabled);
  }
}

void AuthInputRowView::OnImplicitAnimationsCompleted() {
  textfield_->Reset();
  SetVisible(false);
  StopObservingImplicitAnimations();
}

bool AuthInputRowView::IsInputSubmittable() const {
  return !textfield_->GetReadOnly() && !textfield_->GetText().empty();
}

void AuthInputRowView::Submit() {
  DCHECK(IsInputSubmittable());
  for (auto& observer : observers_) {
    observer.OnSubmit(textfield_->GetText());
  }
}

void AuthInputRowView::Escape() {
  for (auto& observer : observers_) {
    observer.OnEscape();
  }
}

void AuthInputRowView::SetCapsLockHighlighted(bool highlight) {
  capslock_icon_->SetImage(highlight ? kCapslockIconHighlighted
                                     : kCapslockIconBlurred);
}

void AuthInputRowView::SetAccessibleNameOnTextfield(
    const std::u16string& new_name) {
  textfield_->SetAccessibleName(new_name);
}

void AuthInputRowView::SetInputEnabled(bool enabled) {
  SetEnabled(enabled);
  textfield_->SetEnabled(enabled);
  textfield_->SetBorder(nullptr);
  submit_button_->SetEnabled(enabled);
  display_text_button_->SetEnabled(enabled);
}

void AuthInputRowView::InsertDigit(int digit) {
  CHECK_EQ(auth_type_, AuthType::kPin);
  textfield_->InsertDigit(digit);
}

void AuthInputRowView::Backspace() {
  CHECK_EQ(auth_type_, AuthType::kPin);
  textfield_->Backspace();
}

void AuthInputRowView::ResetState() {
  // This automatically calls the OnContentsChanged function, and that will set
  // the buttons properly to the reset state.
  textfield_->Reset();
}

void AuthInputRowView::AddObserver(Observer* observer) {
  observers_.AddObserver(observer);
}

void AuthInputRowView::RemoveObserver(Observer* observer) {
  observers_.RemoveObserver(observer);
}

BEGIN_METADATA(AuthInputRowView)
END_METADATA

}  // namespace ash