File: touch_selection_magnifier_aura.cc

package info (click to toggle)
chromium 138.0.7204.183-1
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 6,071,908 kB
  • sloc: cpp: 34,937,088; ansic: 7,176,967; javascript: 4,110,704; python: 1,419,953; asm: 946,768; xml: 739,971; pascal: 187,324; sh: 89,623; perl: 88,663; objc: 79,944; sql: 50,304; cs: 41,786; fortran: 24,137; makefile: 21,806; php: 13,980; tcl: 13,166; yacc: 8,925; ruby: 7,485; awk: 3,720; lisp: 3,096; lex: 1,327; ada: 727; jsp: 228; sed: 36
file content (323 lines) | stat: -rw-r--r-- 14,654 bytes parent folder | download | duplicates (3)
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