chromium/ash/keyboard/ui/container_floating_behavior.cc

// Copyright 2017 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/keyboard/ui/container_floating_behavior.h"

#include <memory>
#include <optional>

#include "ash/keyboard/ui/display_util.h"
#include "ash/keyboard/ui/drag_descriptor.h"
#include "base/numerics/safe_conversions.h"
#include "ui/aura/window.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/display/display.h"
#include "ui/events/event.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/size.h"
#include "ui/wm/core/window_animations.h"

namespace keyboard {

// The virtual keyboard show/hide animation durations.
constexpr auto kShowAnimationDuration = base::Milliseconds(200);
constexpr auto kHideAnimationDuration = base::Milliseconds(100);

// Distance the keyboard moves during the animation
constexpr int kAnimationDistance = 30;

ContainerFloatingBehavior::ContainerFloatingBehavior(Delegate* delegate)
    : ContainerBehavior(delegate) {}

ContainerFloatingBehavior::~ContainerFloatingBehavior() = default;

ContainerType ContainerFloatingBehavior::GetType() const {
  return ContainerType::kFloating;
}

void ContainerFloatingBehavior::DoHidingAnimation(
    aura::Window* container,
    ::wm::ScopedHidingAnimationSettings* animation_settings) {
  animation_settings->layer_animation_settings()->SetTransitionDuration(
      kHideAnimationDuration);
  gfx::Transform transform;
  transform.Translate(0, kAnimationDistance);
  container->SetTransform(transform);
  container->layer()->SetOpacity(0.f);
}

void ContainerFloatingBehavior::DoShowingAnimation(
    aura::Window* container,
    ui::ScopedLayerAnimationSettings* animation_settings) {
  animation_settings->SetTweenType(gfx::Tween::LINEAR_OUT_SLOW_IN);
  animation_settings->SetTransitionDuration(kShowAnimationDuration);

  container->SetTransform(gfx::Transform());
  container->layer()->SetOpacity(1.0);
}

void ContainerFloatingBehavior::InitializeShowAnimationStartingState(
    aura::Window* container) {
  aura::Window* root_window = container->GetRootWindow();

  SetCanonicalBounds(container, root_window->bounds());

  gfx::Transform transform;
  transform.Translate(0, kAnimationDistance);
  container->SetTransform(transform);
  container->layer()->SetOpacity(kAnimationStartOrAfterHideOpacity);
}

gfx::Rect ContainerFloatingBehavior::AdjustSetBoundsRequest(
    const gfx::Rect& display_bounds,
    const gfx::Rect& requested_bounds_in_screen) {
  gfx::Rect keyboard_bounds_in_screen = ContainKeyboardToDisplayBounds(
      requested_bounds_in_screen, display_bounds);
  SavePosition(keyboard_bounds_in_screen, display_bounds.size());
  return keyboard_bounds_in_screen;
}

void ContainerFloatingBehavior::SavePosition(
    const gfx::Rect& keyboard_bounds_in_screen,
    const gfx::Size& screen_size) {
  int left_distance = keyboard_bounds_in_screen.x();
  int right_distance = screen_size.width() - keyboard_bounds_in_screen.right();
  int top_distance = keyboard_bounds_in_screen.y();
  int bottom_distance =
      screen_size.height() - keyboard_bounds_in_screen.bottom();

  double available_width = left_distance + right_distance;
  double available_height = top_distance + bottom_distance;

  if (!default_position_in_screen_) {
    default_position_in_screen_ = std::make_unique<KeyboardPosition>();
  }

  default_position_in_screen_->left_padding_allotment_ratio =
      left_distance / available_width;
  default_position_in_screen_->top_padding_allotment_ratio =
      top_distance / available_height;
}

gfx::Rect ContainerFloatingBehavior::ConvertAreaInKeyboardToScreenBounds(
    const gfx::Rect& area_in_keyboard_window,
    const gfx::Rect& keyboard_window_bounds_in_display) const {
  gfx::Point origin_in_screen(
      keyboard_window_bounds_in_display.x() + area_in_keyboard_window.x(),
      keyboard_window_bounds_in_display.y() + area_in_keyboard_window.y());
  return gfx::Rect(origin_in_screen, area_in_keyboard_window.size());
}

gfx::Rect ContainerFloatingBehavior::GetBoundsWithinDisplay(
    const gfx::Rect& bounds,
    const gfx::Rect& display_bounds) const {
  gfx::Rect new_bounds = bounds;

  if (bounds.x() < display_bounds.x()) {
    new_bounds.set_origin(gfx::Point(display_bounds.x(), new_bounds.y()));
  }
  if (bounds.right() >= display_bounds.right()) {
    new_bounds.set_origin(
        gfx::Point(display_bounds.right() - bounds.width(), new_bounds.y()));
  }
  if (bounds.y() < display_bounds.y()) {
    new_bounds.set_origin(gfx::Point(new_bounds.x(), display_bounds.y()));
  }
  if (bounds.bottom() >= display_bounds.bottom()) {
    new_bounds.set_origin(gfx::Point(
        new_bounds.x(), display_bounds.bottom() - new_bounds.height()));
  }

  return new_bounds;
}

gfx::Rect ContainerFloatingBehavior::ContainKeyboardToDisplayBounds(
    const gfx::Rect& keyboard_window_bounds_in_screen,
    const gfx::Rect& display_bounds) const {
  if (!area_in_window_to_remain_on_screen_) {
    return GetBoundsWithinDisplay(keyboard_window_bounds_in_screen,
                                  display_bounds);
  }

  // This area is relative to the origin of the keyboard window not the
  // screen.
  gfx::Rect inner_area_of_keyboard_window =
      *area_in_window_to_remain_on_screen_;

  gfx::Rect area_to_remain_on_display = ConvertAreaInKeyboardToScreenBounds(
      inner_area_of_keyboard_window, keyboard_window_bounds_in_screen);

  gfx::Rect area_constrained_to_display =
      GetBoundsWithinDisplay(area_to_remain_on_display, display_bounds);

  // We need to calculate the new keyboard window bounds in this method,
  // and at the moment we have constrained only an area inside the window
  // to the display, not the entire keyboard window. So now we must
  // derive the containing keyboard window bounds from this constrained
  // inner area.
  gfx::Point containing_keyboard_window_origin(
      area_constrained_to_display.x() - inner_area_of_keyboard_window.x(),
      area_constrained_to_display.y() - inner_area_of_keyboard_window.y());

  return gfx::Rect(containing_keyboard_window_origin,
                   keyboard_window_bounds_in_screen.size());
}

bool ContainerFloatingBehavior::IsOverscrollAllowed() const {
  return false;
}

gfx::Point ContainerFloatingBehavior::GetPositionForShowingKeyboard(
    const gfx::Size& keyboard_size,
    const gfx::Rect& display_bounds) const {
  // Start with the last saved position
  gfx::Point top_left_offset;
  KeyboardPosition* position = default_position_in_screen_.get();
  if (position == nullptr) {
    // If there is none, center the keyboard along the bottom of the screen.
    top_left_offset.set_x(display_bounds.width() - keyboard_size.width() -
                          kDefaultDistanceFromScreenRight);
    top_left_offset.set_y(display_bounds.height() - keyboard_size.height() -
                          kDefaultDistanceFromScreenBottom);
  } else {
    double left = (display_bounds.width() - keyboard_size.width()) *
                  position->left_padding_allotment_ratio;
    double top = (display_bounds.height() - keyboard_size.height()) *
                 position->top_padding_allotment_ratio;
    top_left_offset.set_x(base::ClampFloor(left));
    top_left_offset.set_y(base::ClampFloor(top));
  }

  // Make sure that this location is valid according to the current size of the
  // screen.
  gfx::Rect keyboard_bounds =
      gfx::Rect(top_left_offset.x() + display_bounds.x(),
                top_left_offset.y() + display_bounds.y(), keyboard_size.width(),
                keyboard_size.height());

  gfx::Rect valid_keyboard_bounds =
      ContainKeyboardToDisplayBounds(keyboard_bounds, display_bounds);

  return valid_keyboard_bounds.origin();
}

bool ContainerFloatingBehavior::HandlePointerEvent(
    const ui::LocatedEvent& event,
    const display::Display& current_display) {
  const gfx::Vector2d kb_offset(base::ClampFloor(event.x()),
                                base::ClampFloor(event.y()));

  const gfx::Rect& keyboard_bounds_in_screen = delegate_->GetBoundsInScreen();

  // Don't handle events if this runs in a partially initialized state.
  if (keyboard_bounds_in_screen.height() <= 0)
    return false;

  ui::PointerId pointer_id = ui::kPointerIdMouse;
  if (event.IsTouchEvent()) {
    const ui::TouchEvent* te = event.AsTouchEvent();
    pointer_id = te->pointer_details().id;
  }

  const ui::EventType type = event.type();
  switch (type) {
    case ui::EventType::kTouchPressed:
    case ui::EventType::kMousePressed:
      if (!draggable_area_.Contains(kb_offset.x(), kb_offset.y())) {
        drag_descriptor_.reset();
      } else if (type == ui::EventType::kMousePressed &&
                 !static_cast<const ui::MouseEvent&>(event)
                      .IsOnlyLeftMouseButton()) {
        // Mouse events are limited to just the left mouse button.
        drag_descriptor_.reset();
      } else if (!drag_descriptor_) {
        drag_descriptor_ = std::make_unique<DragDescriptor>(DragDescriptor{
            keyboard_bounds_in_screen.origin(), kb_offset, pointer_id});
      }
      break;

    case ui::EventType::kMouseDragged:
    case ui::EventType::kTouchMoved:
      if (drag_descriptor_ && drag_descriptor_->pointer_id == pointer_id) {
        // Drag continues.
        // If there is an active drag, use it to determine the new location
        // of the keyboard.
        const gfx::Point original_click_location =
            drag_descriptor_->original_keyboard_location +
            drag_descriptor_->original_click_offset;
        const gfx::Point current_drag_location =
            keyboard_bounds_in_screen.origin() + kb_offset;
        const gfx::Vector2d cumulative_drag_offset =
            current_drag_location - original_click_location;
        const gfx::Point new_keyboard_location =
            drag_descriptor_->original_keyboard_location +
            cumulative_drag_offset;
        gfx::Rect new_bounds_in_local =
            gfx::Rect(new_keyboard_location, keyboard_bounds_in_screen.size());

        DisplayUtil display_util;
        const display::Display& new_display =
            display_util.FindAdjacentDisplayIfPointIsNearMargin(
                current_display, current_drag_location);

        if (current_display.id() == new_display.id()) {
          delegate_->MoveKeyboardWindow(new_bounds_in_local);
        } else {
          // Since the keyboard has jumped across screens, cancel the current
          // drag descriptor as though the user has lifted their finger.
          drag_descriptor_.reset();

          gfx::Rect new_bounds_in_screen =
              new_bounds_in_local +
              current_display.bounds().origin().OffsetFromOrigin();
          gfx::Rect contained_new_bounds_in_screen =
              ContainKeyboardToDisplayBounds(new_bounds_in_screen,
                                             new_display.bounds());

          // Enqueue a transition to the adjacent display.
          new_bounds_in_local =
              contained_new_bounds_in_screen -
              new_display.bounds().origin().OffsetFromOrigin();
          delegate_->MoveKeyboardWindowToDisplay(new_display,
                                                 new_bounds_in_local);
        }
        SavePosition(delegate_->GetBoundsInScreen(), new_display.size());
        return true;
      }
      break;

    default:
      drag_descriptor_.reset();
      break;
  }
  return false;
}

bool ContainerFloatingBehavior::HandleGestureEvent(
    const ui::GestureEvent& event,
    const gfx::Rect& bounds_in_screen) {
  return false;
}

void ContainerFloatingBehavior::SetCanonicalBounds(
    aura::Window* container,
    const gfx::Rect& display_bounds) {
  gfx::Point keyboard_location =
      GetPositionForShowingKeyboard(container->bounds().size(), display_bounds);
  gfx::Rect keyboard_bounds_in_screen =
      gfx::Rect(keyboard_location, container->bounds().size());
  SavePosition(keyboard_bounds_in_screen, display_bounds.size());
  container->SetBounds(keyboard_bounds_in_screen);
}

bool ContainerFloatingBehavior::TextBlurHidesKeyboard() const {
  return true;
}

gfx::Rect ContainerFloatingBehavior::GetOccludedBounds(
    const gfx::Rect& visual_bounds_in_screen) const {
  return {};
}

bool ContainerFloatingBehavior::OccludedBoundsAffectWorkspaceLayout() const {
  return false;
}

void ContainerFloatingBehavior::SetDraggableArea(const gfx::Rect& rect) {
  draggable_area_ = rect;
}

void ContainerFloatingBehavior::SetAreaToRemainOnScreen(const gfx::Rect& rect) {
  area_in_window_to_remain_on_screen_ = rect;
}

}  //  namespace keyboard