blob: b2da53e7c6062075e37f45d4fce522a0a464ee37 [file] [log] [blame]
// 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 "ui/touch_selection/touch_selection_magnifier_aura.h"
#include "cc/paint/paint_flags.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/color/color_provider_manager.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_delegate.h"
#include "ui/compositor/paint_recorder.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/geometry/point_conversions.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/geometry/size_f.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/gfx/shadow_value.h"
#include "ui/gfx/skia_paint_util.h"
#include "ui/native_theme/native_theme.h"
#include "ui/native_theme/native_theme_observer.h"
namespace ui {
namespace {
constexpr float kMagnifierScale = 1.25f;
constexpr int kMagnifierRadius = 20;
// Duration of the animation when updating magnifier bounds.
constexpr base::TimeDelta kMagnifierTransitionDuration = base::Milliseconds(50);
// Size of the zoomed contents, which excludes border and shadows.
constexpr gfx::Size kMagnifierSize{100, 40};
// Offset to apply to the magnifier bounds so that the magnifier is shown
// vertically above the caret (or selection endpoint). The offset specifies
// vertical displacement from the the top of the caret to the bottom of the
// magnifier's zoomed contents. Note that it is negative since the bottom of the
// zoomed contents should be above the top of the caret.
constexpr int kMagnifierVerticalBoundsOffset = -8;
constexpr int kMagnifierBorderThickness = 1;
constexpr float kMagnifierBorderOpacity = 0.2f;
// Shadows to draw around the zoomed contents.
// TODO(b/299966070): Try to unify this with how other shadows are handled.
gfx::ShadowValues GetMagnifierShadowValues(SkColor key_shadow_color,
SkColor ambient_shadow_color) {
constexpr int kShadowElevation = 3;
constexpr int kShadowBlur = 2 * kShadowElevation;
return {gfx::ShadowValue(gfx::Vector2d(0, kShadowElevation), kShadowBlur,
key_shadow_color),
gfx::ShadowValue(gfx::Vector2d(), kShadowBlur, ambient_shadow_color)};
}
// The space outside the zoom layer needed for shadows.
gfx::Outsets GetMagnifierShadowOutsets() {
return gfx::ShadowValue::GetMargin(
GetMagnifierShadowValues(gfx::kPlaceholderColor,
gfx::kPlaceholderColor))
.ToOutsets();
}
// Bounds of the zoom layer in coordinates of its parent. These zoom layer
// bounds are fixed since we only update the bounds of the parent magnifier
// layer when the magnifier moves.
gfx::Rect GetZoomLayerBounds() {
const gfx::Outsets magnifier_shadow_outsets = GetMagnifierShadowOutsets();
return gfx::Rect(magnifier_shadow_outsets.left(),
magnifier_shadow_outsets.top(), kMagnifierSize.width(),
kMagnifierSize.height());
}
// Size of the border layer, which includes space for the zoom layer and
// surrounding border and shadows.
gfx::Size GetBorderLayerSize() {
return kMagnifierSize + GetMagnifierShadowOutsets().size();
}
// Gets the bounds at which to show the magnifier layer. We try to horizontally
// center the magnifier above `anchor_point`, but adjust if needed to keep it
// within the parent bounds. `anchor_point` and returned bounds should be in
// coordinates of the magnifier's parent container.
gfx::Rect GetMagnifierLayerBounds(const gfx::Size& parent_container_size,
const gfx::Point& anchor_point) {
// Compute bounds for the magnifier zoomed contents, which we try to
// horizontally center above `anchor_point`.
const gfx::Point origin(anchor_point.x() - kMagnifierSize.width() / 2,
anchor_point.y() - kMagnifierSize.height() +
kMagnifierVerticalBoundsOffset);
gfx::Rect magnifier_layer_bounds(origin, kMagnifierSize);
// Outset the bounds to account for the magnifier border and shadows.
magnifier_layer_bounds.Outset(GetMagnifierShadowOutsets());
// Adjust if needed to keep the magnifier layer within the parent container
// bounds, while keeping the magnifier size fixed.
magnifier_layer_bounds.AdjustToFit(gfx::Rect(parent_container_size));
return magnifier_layer_bounds;
}
// Gets the center of the source content to be shown in the magnifier. We try to
// center the source content on `focus_center`, but adjust if needed to keep the
// source content within the parent bounds. `focus_center` and returned source
// center should be in coordinates of the magnifier's parent container.
gfx::Point GetMagnifierSourceCenter(const gfx::Size& parent_container_size,
const gfx::Point& focus_center) {
const gfx::SizeF source_size(kMagnifierSize.width() / kMagnifierScale,
kMagnifierSize.height() / kMagnifierScale);
const gfx::PointF source_origin(focus_center.x() - source_size.width() / 2,
focus_center.y() - source_size.height() / 2);
gfx::RectF source_bounds(source_origin, source_size);
source_bounds.AdjustToFit(gfx::RectF(parent_container_size));
return gfx::ToRoundedPoint(source_bounds.CenterPoint());
}
} // namespace
// Delegate for drawing the magnifier border and shadows onto the border layer.
class TouchSelectionMagnifierAura::BorderRenderer : public LayerDelegate {
public:
BorderRenderer() { UpdateTheme(NativeTheme::GetInstanceForNativeUi()); }
BorderRenderer(const BorderRenderer&) = delete;
BorderRenderer& operator=(const BorderRenderer&) = delete;
~BorderRenderer() override = default;
// LayerDelegate:
void OnPaintLayer(const PaintContext& context) override {
PaintRecorder recorder(context, GetBorderLayerSize());
const gfx::Rect zoom_layer_bounds = GetZoomLayerBounds();
// Draw shadows onto the border layer. These shadows should surround the
// zoomed contents, so we draw them around the zoom layer bounds.
cc::PaintFlags shadow_flags;
shadow_flags.setAntiAlias(true);
shadow_flags.setColor(SK_ColorTRANSPARENT);
shadow_flags.setLooper(gfx::CreateShadowDrawLooper(
GetMagnifierShadowValues(key_shadow_color_, ambient_shadow_color)));
recorder.canvas()->DrawRoundRect(zoom_layer_bounds, kMagnifierRadius,
shadow_flags);
// Since the border layer is stacked above the zoom layer (to prevent the
// magnifier border and shadows from being magnified), we now need to clear
// the parts of the shadow covering the zoom layer.
cc::PaintFlags mask_flags;
mask_flags.setAntiAlias(true);
mask_flags.setBlendMode(SkBlendMode::kClear);
mask_flags.setStyle(cc::PaintFlags::kFill_Style);
recorder.canvas()->DrawRoundRect(zoom_layer_bounds, kMagnifierRadius,
mask_flags);
// Draw the magnifier border onto the border layer, using the zoom layer
// bounds so that the border surrounds the zoomed contents.
cc::PaintFlags border_flags;
border_flags.setAntiAlias(true);
border_flags.setStyle(cc::PaintFlags::kStroke_Style);
border_flags.setStrokeWidth(kMagnifierBorderThickness);
border_flags.setColor(border_color_);
border_flags.setAlphaf(kMagnifierBorderOpacity);
recorder.canvas()->DrawRoundRect(zoom_layer_bounds, kMagnifierRadius,
border_flags);
}
void OnDeviceScaleFactorChanged(float old_device_scale_factor,
float new_device_scale_factor) override {}
void UpdateTheme(NativeTheme* theme) {
auto* color_provider = ColorProviderManager::Get().GetColorProviderFor(
theme->GetColorProviderKey(nullptr));
border_color_ = color_provider->GetColor(kColorSeparator);
key_shadow_color_ =
color_provider->GetColor(kColorShadowValueKeyShadowElevationThree);
ambient_shadow_color =
color_provider->GetColor(kColorShadowValueAmbientShadowElevationThree);
}
private:
SkColor border_color_ = gfx::kPlaceholderColor;
SkColor key_shadow_color_ = gfx::kPlaceholderColor;
SkColor ambient_shadow_color = gfx::kPlaceholderColor;
};
TouchSelectionMagnifierAura::TouchSelectionMagnifierAura() {
CreateMagnifierLayer();
theme_observation_.Observe(NativeTheme::GetInstanceForNativeUi());
}
TouchSelectionMagnifierAura::~TouchSelectionMagnifierAura() = default;
void TouchSelectionMagnifierAura::ShowFocusBound(Layer* parent,
const gfx::Point& focus_start,
const gfx::Point& focus_end) {
DCHECK(parent);
if (magnifier_layer_->parent() != parent) {
// Hide the magnifier when parenting or reparenting the magnifier so that it
// doesn't appear with the wrong bounds.
magnifier_layer_->SetVisible(false);
parent->Add(magnifier_layer_.get());
}
// Set up the animation for updating the magnifier bounds.
ScopedLayerAnimationSettings settings(magnifier_layer_->GetAnimator());
if (!magnifier_layer_->IsVisible()) {
// Set the magnifier to appear immediately once its bounds are set.
settings.SetTransitionDuration(base::Milliseconds(0));
settings.SetTweenType(gfx::Tween::ZERO);
settings.SetPreemptionStrategy(LayerAnimator::IMMEDIATELY_SET_NEW_TARGET);
} else {
// Set the magnifier to move smoothly from its current bounds to the updated
// bounds.
settings.SetTransitionDuration(kMagnifierTransitionDuration);
settings.SetTweenType(gfx::Tween::LINEAR);
settings.SetPreemptionStrategy(
LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
}
const gfx::Size magnifier_parent_size =
magnifier_layer_->parent()->bounds().size();
const gfx::Rect focus_rect = gfx::BoundingRect(focus_start, focus_end);
const gfx::Rect magnifier_layer_bounds =
GetMagnifierLayerBounds(magnifier_parent_size, focus_rect.top_center());
magnifier_layer_->SetBounds(magnifier_layer_bounds);
// Compute the background offset to center the zoomed contents on the source
// center. Note that the zoom layer center here is not the same as the
// magnifier layer center, since the magnifier layer includes non-uniform
// shadows that surround the zoomed contents.
gfx::Rect zoom_layer_bounds = GetZoomLayerBounds();
const gfx::Point magnifier_source_center =
GetMagnifierSourceCenter(magnifier_parent_size, focus_rect.CenterPoint());
const gfx::Point zoom_layer_center =
zoom_layer_bounds.CenterPoint() +
magnifier_layer_bounds.OffsetFromOrigin();
const gfx::Point zoom_offset =
gfx::PointAtOffsetFromOrigin(zoom_layer_center - magnifier_source_center);
// The zoom_layer_ is relative to the magnifier_layer_ widget, which has been
// shifted to avoid overlapping the content of the zoom. Un-shift the zoom
// bounds so that its layer corresponds directly with what will be magnified
// (backdrop filters can only access pixels under their own layer). Then use
// a regular filter to offset the zoom's output to align with the magnifier.
zoom_layer_bounds.Offset(-zoom_offset.x(), -zoom_offset.y());
zoom_layer_->SetBounds(zoom_layer_bounds);
zoom_layer_->SetLayerOffset(zoom_offset);
if (!magnifier_layer_->IsVisible()) {
magnifier_layer_->SetVisible(true);
}
}
void TouchSelectionMagnifierAura::OnNativeThemeUpdated(
NativeTheme* observed_theme) {
border_renderer_->UpdateTheme(observed_theme);
border_layer_->SchedulePaint(gfx::Rect(border_layer_->size()));
}
gfx::Rect TouchSelectionMagnifierAura::GetZoomedContentsBoundsForTesting()
const {
// The zoom_layer_ bounds are the upscaled source bounds, so we have to undo
// that scaling to get the true contents bounds (undoes the logic inside
// GetMagnifierSourceCenter()).
const gfx::RectF bounds{zoom_layer_->bounds()};
const gfx::PointF center = bounds.CenterPoint();
const gfx::SizeF contents_size(bounds.width() / kMagnifierScale,
bounds.height() / kMagnifierScale);
const gfx::PointF contents_origin(center.x() - contents_size.width() / 2,
center.y() - contents_size.height() / 2);
gfx::Rect contents_bounds =
gfx::ToEnclosingRect(gfx::RectF(contents_origin, contents_size));
// The zoomed contents is drawn by the zoom layer. We just need to convert its
// bounds to coordinates of the magnifier layer's parent layer.
return contents_bounds + magnifier_layer_->bounds().OffsetFromOrigin();
}
gfx::Rect TouchSelectionMagnifierAura::GetMagnifierBoundsForTesting() const {
return magnifier_layer_->bounds();
}
const Layer* TouchSelectionMagnifierAura::GetMagnifierParentForTesting() const {
return magnifier_layer_->parent();
}
void TouchSelectionMagnifierAura::CreateMagnifierLayer() {
// Create the magnifier layer, which will parent the zoom layer and border
// layer.
magnifier_layer_ = std::make_unique<Layer>(LAYER_NOT_DRAWN);
magnifier_layer_->SetName("TouchSelectionMagnifierAura/MagnifierLayer");
magnifier_layer_->SetFillsBoundsOpaquely(false);
// Create the zoom layer, which will show the zoomed contents.
zoom_layer_ = std::make_unique<Layer>(LAYER_SOLID_COLOR);
zoom_layer_->SetName("TouchSelectionMagnifierAura/ZoomLayer");
zoom_layer_->SetBackgroundZoom(kMagnifierScale, 0);
// BackdropFilterBounds applies after the backdrop filter (the zoom effect)
// but before anything else, meaning its clipping effect is transformed by
// the layer_offset() filter operation. SetRoundedCornerRadius() applies too
// late and is affected by the layer offset, so would incorrectly clip the
// zoomed contents.
zoom_layer_->SetBackdropFilterBounds(
gfx::RRectF{gfx::RectF(kMagnifierSize), kMagnifierRadius});
magnifier_layer_->Add(zoom_layer_.get());
// Create the border layer. This is stacked above the zoom layer so that the
// magnifier border and shadows aren't shown in the zoomed contents drawn by
// the zoom layer.
border_layer_ = std::make_unique<Layer>();
border_layer_->SetName("TouchSelectionMagnifierAura/BorderLayer");
border_layer_->SetBounds(gfx::Rect(GetBorderLayerSize()));
border_renderer_ = std::make_unique<BorderRenderer>();
border_layer_->set_delegate(border_renderer_.get());
border_layer_->SetFillsBoundsOpaquely(false);
magnifier_layer_->Add(border_layer_.get());
}
} // namespace ui