chromium/ash/style/system_textfield.cc

// Copyright 2022 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/style/system_textfield.h"

#include <optional>

#include "ash/style/ash_color_id.h"
#include "ash/style/system_textfield_controller.h"
#include "ash/style/typography.h"
#include "ash/wm/work_area_insets.h"
#include "chromeos/constants/chromeos_features.h"
#include "ui/aura/client/screen_position_client.h"
#include "ui/aura/env.h"
#include "ui/aura/window.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/color/color_provider.h"
#include "ui/events/event_handler.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/canvas.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/focus_ring.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/wm/core/coordinate_conversion.h"

namespace ash {

namespace {

// The heights of textfield containers for different font sizes.
constexpr int kSmallContainerHeight = 24;
constexpr int kMediumContainerHeight = 28;
constexpr int kLargeContainerHeight = 28;
// The minimum textfield container width.
constexpr int kMinWidth = 80;
// The gap between the focus ring and textfield container.
constexpr float kFocusRingGap = 2.0f;
// The border insets to add horizontal paddings in container.
constexpr gfx::Insets kBorderInsets = gfx::Insets::VH(0, 8);
// The rounded conner radius of textfield container.
constexpr int kCornerRadius = 4;

// Gets textfield container heights for different types.
int GetContainerHeightFromType(SystemTextfield::Type type) {
  int container_height;
  switch (type) {
    case SystemTextfield::Type::kSmall:
      container_height = kSmallContainerHeight;
      break;
    case SystemTextfield::Type::kMedium:
      container_height = kMediumContainerHeight;
      break;
    case SystemTextfield::Type::kLarge:
      container_height = kLargeContainerHeight;
      break;
  }
  return container_height;
}

// Gets font list for different types.
gfx::FontList GetFontListFromType(SystemTextfield::Type type) {
  TypographyToken token;
  switch (type) {
    case SystemTextfield::Type::kSmall:
      token = TypographyToken::kCrosAnnotation1;
      break;
    case SystemTextfield::Type::kMedium:
      token = TypographyToken::kCrosBody1;
      break;
    case SystemTextfield::Type::kLarge:
      token = TypographyToken::kCrosBody0;
      break;
  }
  return TypographyProvider::Get()->ResolveTypographyToken(token);
}

}  // namespace

//------------------------------------------------------------------------------
// SystemTextfield::EventHandler:
// Used to handle the case when user wants to commit the changes by clicking
// outside the textfield.
// TODO(b/312226702): should fix remaining issues: 1. it does not handle
// the touch event, 2. the changes can only be committed when clicking within
// the widget.
class SystemTextfield::EventHandler : public ui::EventHandler {
 public:
  explicit EventHandler(SystemTextfield* textfield) : textfield_(textfield) {
    aura::Env::GetInstance()->AddPreTargetHandler(this);
  }

  EventHandler(const EventHandler&) = delete;
  EventHandler& operator=(const EventHandler&) = delete;
  ~EventHandler() override {
    aura::Env::GetInstance()->RemovePreTargetHandler(this);
  }

  // ui::EventHandler:
  void OnMouseEvent(ui::MouseEvent* event) override { OnLocatedEvent(event); }
  void OnTouchEvent(ui::TouchEvent* event) override { OnLocatedEvent(event); }

 private:
  void OnLocatedEvent(ui::LocatedEvent* event) {
    if (!textfield_->IsActive()) {
      return;
    }

    const ui::EventType event_type = event->type();
    if (event_type != ui::EventType::kMousePressed) {
      return;
    }

    // Do not handle the pre-target event if the context menu is showing.
    if (textfield_->IsMenuShowing()) {
      return;
    }

    // Get event location in screen.
    gfx::Point event_location = event->location();
    aura::Window* event_target = static_cast<aura::Window*>(event->target());

    if (!aura::client::GetScreenPositionClient(event_target->GetRootWindow())) {
      return;
    }

    wm::ConvertPointToScreen(event_target, &event_location);

    const bool event_in_textfield =
        textfield_->GetBoundsInScreen().Contains(event_location);

    // If a clicking event happens outside the textfield, commit the
    // changes and deactivate the textfield.
    if (!event_in_textfield) {
      textfield_->SetActive(false);
    }
  }

  raw_ptr<SystemTextfield> textfield_;
};

//------------------------------------------------------------------------------
// SystemTextfield::SystemTextfield:
SystemTextfield::SystemTextfield(Type type)
    : type_(type),
      event_handler_(std::make_unique<EventHandler>(this)),
      corner_radius_(kCornerRadius) {
  SetFontList(GetFontListFromType(type_));
  SetBorder(views::CreateEmptyBorder(kBorderInsets));
  // Remove the default hover effect, since the hover effect of system textfield
  // appears not only on hover but also on focus.
  RemoveHoverEffect();

  // Override the very round highlight path set in `views::Textfield`.
  views::InstallRoundRectHighlightPathGenerator(this, gfx::Insets(),
                                                corner_radius_);

  // Configure focus ring.
  auto* focus_ring = views::FocusRing::Get(this);
  DCHECK(focus_ring);
  focus_ring->SetOutsetFocusRingDisabled(true);
  const float halo_thickness = focus_ring->GetHaloThickness();
  focus_ring->SetHaloInset(-kFocusRingGap - 0.5f * halo_thickness);
  focus_ring->SetColorId(cros_tokens::kCrosSysFocusRing);
  focus_ring->SetHasFocusPredicate(base::BindRepeating(
      [](const SystemTextfield* textfield, const views::View* view) {
        return textfield->show_focus_ring_;
      },
      base::Unretained(this)));

  enabled_changed_subscription_ = AddEnabledChangedCallback(base::BindRepeating(
      &SystemTextfield::OnEnabledStateChanged, base::Unretained(this)));
}

SystemTextfield::~SystemTextfield() = default;

void SystemTextfield::SetTextColorId(ui::ColorId color_id) {
  UpdateColorId(text_color_id_, color_id, /*is_background_color=*/false);
}

void SystemTextfield::SetSelectedTextColorId(ui::ColorId color_id) {
  UpdateColorId(selected_text_color_id_, color_id,
                /*is_background_color=*/false);
}

void SystemTextfield::SetSelectionBackgroundColorId(ui::ColorId color_id) {
  UpdateColorId(selection_background_color_id_, color_id,
                /*is_background_color=*/false);
}

void SystemTextfield::SetBackgroundColorId(ui::ColorId color_id) {
  UpdateColorId(background_color_id_, color_id, /*is_background_color=*/true);
}

void SystemTextfield::SetPlaceholderTextColorId(ui::ColorId color_id) {
  UpdateColorId(placeholder_text_color_id_, color_id,
                /*is_background_color=*/false);
}

void SystemTextfield::SetActiveStateChangedCallback(
    base::RepeatingClosure callback) {
  active_state_changed_callback_ = std::move(callback);
}

void SystemTextfield::SetCornerRadius(int corner_radius) {
  corner_radius_ = corner_radius;

  views::InstallRoundRectHighlightPathGenerator(this, gfx::Insets(),
                                                corner_radius_);
  UpdateBackground();
}

void SystemTextfield::SetActive(bool active) {
  if (IsActive() == active) {
    return;
  }

  if (active) {
    // Activate the textfield and record the text content.
    views::Textfield::OnFocus();
    restored_text_content_ = GetText();
  } else {
    // Clear selection when the textfield is deactivated.
    ClearSelection();
    views::Textfield::OnBlur();
  }

  SetShowFocusRing(active);
  UpdateBackground();
  if (active_state_changed_callback_) {
    active_state_changed_callback_.Run();
  }
}

bool SystemTextfield::IsActive() const {
  return GetRenderText()->focused();
}

void SystemTextfield::SetShowFocusRing(bool show) {
  if (show_focus_ring_ == show) {
    return;
  }
  show_focus_ring_ = show;

  // It's possible that derived classes could have removed the focus ring.
  if (auto* focus_ring = views::FocusRing::Get(this); focus_ring != nullptr) {
    focus_ring->SetOutsetFocusRingDisabled(true);
    focus_ring->SchedulePaint();
  }
}

void SystemTextfield::SetShowBackground(bool show) {
  show_background_ = show;
  UpdateBackground();
}

void SystemTextfield::RestoreText() {
  SetText(restored_text_content_);
}

void SystemTextfield::UpdateBackground() {
  const bool has_background =
      GetBackgroundEnabled() &&
      (IsMouseHovered() || HasFocus() || show_background_);
  if (!has_background) {
    SetBackground(nullptr);
    return;
  }

  SetBackground(views::CreateThemedRoundedRectBackground(
      background_color_id_.value_or(cros_tokens::kCrosSysHoverOnSubtle),
      corner_radius_));
}

gfx::Size SystemTextfield::CalculatePreferredSize(
    const views::SizeBounds& available_size) const {
  // The width of container equals to the content width with horizontal padding.
  // The height of the container dependents on the type.
  const std::u16string& text = GetText();
  int width = 0;
  int height = 0;
  gfx::Canvas::SizeStringInt(text.empty() ? GetPlaceholderText() : text,
                             GetFontListFromType(type_), &width, &height, 0,
                             gfx::Canvas::NO_ELLIPSIS);
  return gfx::Size(
      std::max(width + GetCaretBounds().width() + GetInsets().width(),
               kMinWidth),
      GetContainerHeightFromType(type_));
}

void SystemTextfield::SetBorder(std::unique_ptr<views::Border> b) {
  // The base `Textfield` has a preset border. When a new border is set, the
  // focus ring will be removed. The `SystemTextfield` needs an empty border for
  // horizontal padding and keeps the focus ring.
  views::View::SetBorder(std::move(b));
}

void SystemTextfield::OnMouseEntered(const ui::MouseEvent& event) {
  UpdateBackground();
}

void SystemTextfield::OnMouseExited(const ui::MouseEvent& event) {
  UpdateBackground();
}

void SystemTextfield::OnThemeChanged() {
  views::View::OnThemeChanged();

  // Only update the text color since the background color will be handled by
  // themed background.
  UpdateTextColor();
}

void SystemTextfield::OnFocus() {
  SetActive(true);
}

void SystemTextfield::OnBlur() {
  // TODO(b/323054951): Remove this when we can correctly handle our peculiar
  // blur logic.
  UpdateCursorVisibility();

  // Call SetActive last because some callbacks might delete `this`.
  SetActive(false);
}

void SystemTextfield::OnEnabledStateChanged() {
  UpdateBackground();
  UpdateTextColor();
  SchedulePaint();
}

void SystemTextfield::UpdateColorId(std::optional<ui::ColorId>& src,
                                    ui::ColorId dst,
                                    bool is_background_color) {
  if (src && *src == dst) {
    return;
  }

  src = dst;
  if (is_background_color) {
    UpdateBackground();
  } else {
    UpdateTextColor();
  }
}

void SystemTextfield::UpdateTextColor() {
  if (!GetWidget()) {
    return;
  }

  // Set text color.
  auto* color_provider = GetColorProvider();
  gfx::RenderText* render_text = GetRenderText();
  if (!GetEnabled()) {
    SetColor(color_provider->GetColor(cros_tokens::kCrosSysDisabled));
    return;
  }

  // Set text color and selection text and background (highlight part) colors.
  SetColor(color_provider->GetColor(
      text_color_id_.value_or(cros_tokens::kCrosSysOnSurface)));
  render_text->set_selection_color(color_provider->GetColor(
      selected_text_color_id_.value_or(cros_tokens::kCrosSysOnSurface)));
  render_text->set_selection_background_focused_color(
      color_provider->GetColor(selection_background_color_id_.value_or(
          cros_tokens::kCrosSysHighlightText)));

  // Set placeholder text color
  set_placeholder_text_color(color_provider->GetColor(
      placeholder_text_color_id_.value_or(cros_tokens::kCrosSysDisabled)));
}

BEGIN_METADATA(SystemTextfield)
END_METADATA

}  // namespace ash