1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323
|
// 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_->SetFillsBoundsOpaquely(false);
// Create the zoom layer, which will show the zoomed contents.
zoom_layer_ = std::make_unique<Layer>(LAYER_SOLID_COLOR);
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_->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
|