0

Add some styling to ChromeOS touch selection magnifier.

Add border, shadow, zoom amount, and magnifier size according to
magnifier specs in go/cros-tte-specs. Also add some basic animation so
that magnifier bounds are updated more smoothly.

Still need some more work to set the correct bounds/offset
(b/273613374). In the meantime, clean up some of the bounds so that the
bounds referenced by the magnifier layer are all in coordinates of the
magnifier parent container.

Bug: b:244116654, b:264823675
Change-Id: Iabba891042432af75ea6eba27a85c6262aec2f40
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/4337092
Commit-Queue: Michelle Chen <michellegc@google.com>
Reviewed-by: Ahmed Fakhry <afakhry@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1122725}
This commit is contained in:
Michelle
2023-03-27 23:07:31 +00:00
committed by Chromium LUCI CQ
parent 203aace329
commit 30b94564a4
3 changed files with 229 additions and 84 deletions

@ -4,28 +4,76 @@
#include "ash/touch/touch_selection_magnifier_runner_ash.h"
#include "ash/constants/ash_features.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/style/color_util.h"
#include "third_party/skia/include/core/SkDrawLooper.h"
#include "ui/aura/window.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/color/color_provider_source_observer.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/paint_recorder.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/gfx/geometry/point_conversions.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/shadow_value.h"
#include "ui/gfx/skia_paint_util.h"
namespace ash {
namespace {
// Gets the bounds of the magnifier when showing the specified point of
// interest. `point_of_interest` and returned bounds are in root window
// coordinates.
gfx::Rect GetBounds(const gfx::Point& point_of_interest) {
const gfx::Size size = TouchSelectionMagnifierRunnerAsh::kMagnifierLayerSize;
constexpr int kMagnifierRadius = 20;
constexpr int kMagnifierBorderThickness = 1;
const gfx::ShadowValues kMagnifierShadowValues =
gfx::ShadowValue::MakeChromeOSSystemUIShadowValues(3);
// The space outside the zoom layer needed for shadows.
const gfx::Outsets kMagnifierShadowOutsets =
gfx::ShadowValue::GetMargin(kMagnifierShadowValues).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.
const gfx::Rect kZoomLayerBounds =
gfx::Rect(kMagnifierShadowOutsets.left(),
kMagnifierShadowOutsets.top(),
TouchSelectionMagnifierRunnerAsh::kMagnifierSize.width(),
TouchSelectionMagnifierRunnerAsh::kMagnifierSize.height());
// Size of the border layer, which includes space for the zoom layer and
// surrounding border and shadows.
const gfx::Size kBorderLayerSize =
TouchSelectionMagnifierRunnerAsh::kMagnifierSize +
kMagnifierShadowOutsets.size();
// Duration of the animation when updating magnifier bounds.
constexpr base::TimeDelta kMagnifierTransitionDuration = base::Milliseconds(50);
// Gets the bounds of the magnifier layer for showing the specified point of
// interest. These bounds include the magnifier border and shadows.
// `point_of_interest` and returned bounds are in coordinates of the magnifier's
// parent container.
gfx::Rect GetMagnifierLayerBounds(const gfx::Point& point_of_interest) {
const gfx::Size& size = TouchSelectionMagnifierRunnerAsh::kMagnifierSize;
const gfx::Point origin(
point_of_interest.x() - size.width() / 2,
point_of_interest.y() - size.height() / 2 +
TouchSelectionMagnifierRunnerAsh::kMagnifierVerticalOffset);
return gfx::Rect(origin, size);
gfx::Rect magnifier_layer_bounds(origin, size);
magnifier_layer_bounds.Outset(kMagnifierShadowOutsets);
return magnifier_layer_bounds;
}
// Gets the border color using `color_provider_source`. Defaults to black if
// `color_provider_source` is nullptr.
SkColor GetBorderColor(const ui::ColorProviderSource* color_provider_source) {
return color_provider_source
? color_provider_source->GetColorProvider()->GetColor(
cros_tokens::kCrosSysSeparator)
: SkColorSetARGB(51, 0, 0, 0);
}
// Returns the child container in `root` that should parent the magnifier layer.
@ -35,6 +83,59 @@ aura::Window* GetMagnifierParentContainerForRoot(aura::Window* root) {
} // namespace
// Delegate for drawing the magnifier border and shadows onto the border layer.
class TouchSelectionMagnifierRunnerAsh::BorderRenderer
: public ui::LayerDelegate {
public:
explicit BorderRenderer(SkColor border_color) : border_color_(border_color) {}
BorderRenderer(const BorderRenderer&) = delete;
BorderRenderer& operator=(const BorderRenderer&) = delete;
~BorderRenderer() override = default;
void set_border_color(SkColor border_color) { border_color_ = border_color; }
// ui::LayerDelegate:
void OnPaintLayer(const ui::PaintContext& context) override {
ui::PaintRecorder recorder(context, kBorderLayerSize);
// Draw shadows onto the border layer. These shadows should surround the
// magnified area, 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(kMagnifierShadowValues));
recorder.canvas()->DrawRoundRect(kZoomLayerBounds, 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(kZoomLayerBounds, kMagnifierRadius,
mask_flags);
// Draw the magnifier border onto the border layer, using the zoom layer
// bounds so that the border surrounds the magnified area.
cc::PaintFlags border_flags;
border_flags.setAntiAlias(true);
border_flags.setStyle(cc::PaintFlags::kStroke_Style);
border_flags.setStrokeWidth(kMagnifierBorderThickness);
border_flags.setColor(border_color_);
recorder.canvas()->DrawRoundRect(kZoomLayerBounds, kMagnifierRadius,
border_flags);
}
void OnDeviceScaleFactorChanged(float old_device_scale_factor,
float new_device_scale_factor) override {}
private:
SkColor border_color_;
};
TouchSelectionMagnifierRunnerAsh::TouchSelectionMagnifierRunnerAsh() = default;
TouchSelectionMagnifierRunnerAsh::~TouchSelectionMagnifierRunnerAsh() = default;
@ -50,26 +151,47 @@ void TouchSelectionMagnifierRunnerAsh::ShowMagnifier(
aura::Window* root_window = current_context_->GetRootWindow();
DCHECK(root_window);
gfx::PointF position_in_root(position);
aura::Window::ConvertPointToTarget(context, root_window, &position_in_root);
aura::Window* parent_container =
GetMagnifierParentContainerForRoot(root_window);
gfx::PointF position_in_parent(position);
aura::Window::ConvertPointToTarget(context, parent_container,
&position_in_parent);
if (!magnifier_layer_) {
CreateMagnifierLayer(root_window, position_in_root);
CreateMagnifierLayer(parent_container, position_in_parent);
} else {
ui::ScopedLayerAnimationSettings settings(magnifier_layer_->GetAnimator());
settings.SetTransitionDuration(kMagnifierTransitionDuration);
settings.SetTweenType(gfx::Tween::LINEAR);
settings.SetPreemptionStrategy(
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
magnifier_layer_->SetBounds(
GetBounds(gfx::ToRoundedPoint(position_in_root)));
GetMagnifierLayerBounds(gfx::ToRoundedPoint(position_in_parent)));
}
}
void TouchSelectionMagnifierRunnerAsh::CloseMagnifier() {
current_context_ = nullptr;
magnifier_layer_ = nullptr;
zoom_layer_ = nullptr;
border_layer_ = nullptr;
border_renderer_ = nullptr;
Observe(nullptr);
}
bool TouchSelectionMagnifierRunnerAsh::IsRunning() const {
return current_context_ != nullptr;
}
void TouchSelectionMagnifierRunnerAsh::OnColorProviderChanged() {
if (border_renderer_) {
DCHECK(border_layer_);
border_renderer_->set_border_color(
GetBorderColor(GetColorProviderSource()));
border_layer_->SchedulePaint(gfx::Rect(border_layer_->size()));
}
}
const aura::Window*
TouchSelectionMagnifierRunnerAsh::GetCurrentContextForTesting() const {
return current_context_;
@ -80,21 +202,44 @@ const ui::Layer* TouchSelectionMagnifierRunnerAsh::GetMagnifierLayerForTesting()
return magnifier_layer_.get();
}
void TouchSelectionMagnifierRunnerAsh::CreateMagnifierLayer(
aura::Window* root_window,
const gfx::PointF& position_in_root) {
aura::Window* parent_container =
GetMagnifierParentContainerForRoot(root_window);
const ui::Layer* TouchSelectionMagnifierRunnerAsh::GetZoomLayerForTesting()
const {
return zoom_layer_.get();
}
void TouchSelectionMagnifierRunnerAsh::CreateMagnifierLayer(
aura::Window* parent_container,
const gfx::PointF& position_in_parent) {
Observe(ColorUtil::GetColorProviderSourceForWindow(parent_container));
ui::Layer* parent_layer = parent_container->layer();
magnifier_layer_ = std::make_unique<ui::Layer>(ui::LAYER_SOLID_COLOR);
magnifier_layer_->SetBounds(GetBounds(gfx::ToRoundedPoint(position_in_root)));
magnifier_layer_->SetBackgroundZoom(kMagnifierScale, 0);
magnifier_layer_->SetBackgroundOffset(
gfx::Point(0, kMagnifierVerticalOffset));
// Create the magnifier layer, which will parent the zoom layer and border
// layer.
magnifier_layer_ = std::make_unique<ui::Layer>(ui::LAYER_NOT_DRAWN);
magnifier_layer_->SetBounds(
GetMagnifierLayerBounds(gfx::ToRoundedPoint(position_in_parent)));
magnifier_layer_->SetFillsBoundsOpaquely(false);
magnifier_layer_->SetRoundedCornerRadius(kMagnifierRoundedCorners);
parent_layer->Add(magnifier_layer_.get());
// Create the zoom layer, which will show the magnified area.
zoom_layer_ = std::make_unique<ui::Layer>(ui::LAYER_SOLID_COLOR);
zoom_layer_->SetBounds(kZoomLayerBounds);
zoom_layer_->SetBackgroundZoom(kMagnifierScale, 0);
zoom_layer_->SetBackgroundOffset(gfx::Point(0, kMagnifierVerticalOffset));
zoom_layer_->SetFillsBoundsOpaquely(false);
zoom_layer_->SetRoundedCornerRadius(gfx::RoundedCornersF{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 magnified area drawn by
// the zoom layer.
border_layer_ = std::make_unique<ui::Layer>();
border_layer_->SetBounds(gfx::Rect(kBorderLayerSize));
border_renderer_ = std::make_unique<BorderRenderer>(
GetBorderColor(GetColorProviderSource()));
border_layer_->set_delegate(border_renderer_.get());
border_layer_->SetFillsBoundsOpaquely(false);
magnifier_layer_->Add(border_layer_.get());
}
} // namespace ash

@ -7,7 +7,7 @@
#include "ash/ash_export.h"
#include "base/memory/raw_ptr.h"
#include "ui/gfx/geometry/rounded_corners_f.h"
#include "ui/color/color_provider_source_observer.h"
#include "ui/gfx/geometry/size.h"
#include "ui/touch_selection/touch_selection_magnifier_runner.h"
@ -23,7 +23,8 @@ namespace ash {
// Ash implementation for TouchSelectionMagnifierRunner.
class ASH_EXPORT TouchSelectionMagnifierRunnerAsh
: public ui::TouchSelectionMagnifierRunner {
: public ui::TouchSelectionMagnifierRunner,
public ui::ColorProviderSourceObserver {
public:
TouchSelectionMagnifierRunnerAsh();
@ -34,15 +35,14 @@ class ASH_EXPORT TouchSelectionMagnifierRunnerAsh
~TouchSelectionMagnifierRunnerAsh() override;
static constexpr float kMagnifierScale = 2.0f;
static constexpr float kMagnifierScale = 1.25f;
static constexpr gfx::Size kMagnifierLayerSize{100, 48};
static constexpr gfx::RoundedCornersF kMagnifierRoundedCorners{20};
// Size of the magnifier zoom layer, which excludes border and shadows.
static constexpr gfx::Size kMagnifierSize{100, 40};
// Offset to apply so that the magnifier is shown vertically above the point
// of interest. The offset specifies vertical displacement from the center of
// the text selection caret to the center of the magnifier bounds.
// the text selection caret to the center of the magnifier zoom layer.
static constexpr int kMagnifierVerticalOffset = -32;
// ui::TouchSelectionMagnifierRunner:
@ -51,20 +51,37 @@ class ASH_EXPORT TouchSelectionMagnifierRunnerAsh
void CloseMagnifier() override;
bool IsRunning() const override;
// ui::ColorProviderSourceObserver:
void OnColorProviderChanged() override;
const aura::Window* GetCurrentContextForTesting() const;
const ui::Layer* GetMagnifierLayerForTesting() const;
const ui::Layer* GetZoomLayerForTesting() const;
private:
void CreateMagnifierLayer(aura::Window* root_window,
class BorderRenderer;
void CreateMagnifierLayer(aura::Window* parent_container,
const gfx::PointF& position_in_root);
// Current context window in which the magnifier is being shown, or `nullptr`
// if no magnifier is running.
raw_ptr<aura::Window> current_context_ = nullptr;
// The magnifier layer, which draws the background with a zoom filter applied.
// The magnifier layer is the parent of the zoom layer and border layer. The
// layer bounds should be updated when selection updates occur.
std::unique_ptr<ui::Layer> magnifier_layer_;
// Draws the background with a zoom filter applied.
std::unique_ptr<ui::Layer> zoom_layer_;
// Draws a border and shadow. `border_layer_` must be ordered after
// `border_renderer_` so that it is destroyed before `border_renderer_`.
// Otherwise `border_layer_` will have a pointer to a deleted delegate.
std::unique_ptr<BorderRenderer> border_renderer_;
std::unique_ptr<ui::Layer> border_layer_;
};
} // namespace ash

@ -43,6 +43,34 @@ class TouchSelectionMagnifierRunnerAshTest : public NoSessionAshTestBase {
NoSessionAshTestBase::SetUp();
}
// Verifies that the magnifier has the correct bounds given the point of
// interest in context window coordinates.
// TODO(b/273613374): For now, we assume that the context window, root
// window, and magnifier parent container have the same bounds, but in
// practice this might not always be the case. Rewrite these tests once the
// bounds related parts of the magnifier have been cleaned up.
void VerifyMagnifierBounds(gfx::PointF point_of_interest) {
TouchSelectionMagnifierRunnerAsh* magnifier_runner = GetMagnifierRunner();
ASSERT_TRUE(magnifier_runner);
const ui::Layer* magnifier_layer =
magnifier_runner->GetMagnifierLayerForTesting();
const ui::Layer* zoom_layer = magnifier_runner->GetZoomLayerForTesting();
ASSERT_TRUE(magnifier_layer);
ASSERT_TRUE(zoom_layer);
gfx::Rect zoom_layer_bounds_in_context =
zoom_layer->bounds() + magnifier_layer->bounds().OffsetFromOrigin();
EXPECT_EQ(zoom_layer_bounds_in_context.size(),
TouchSelectionMagnifierRunnerAsh::kMagnifierSize);
EXPECT_EQ(
zoom_layer_bounds_in_context.CenterPoint(),
gfx::Point(
point_of_interest.x(),
point_of_interest.y() +
TouchSelectionMagnifierRunnerAsh::kMagnifierVerticalOffset));
}
private:
base::test::ScopedFeatureList scoped_feature_list_;
};
@ -106,79 +134,34 @@ TEST_F(TouchSelectionMagnifierRunnerAshTest, NewContext) {
EXPECT_FALSE(magnifier_runner->GetCurrentContextForTesting());
}
// Tests that the magnifier layer is created and destroyed.
TEST_F(TouchSelectionMagnifierRunnerAshTest, Layer) {
// Tests that the magnifier and zoom layers are created and destroyed.
TEST_F(TouchSelectionMagnifierRunnerAshTest, CreatesAndDestroysLayers) {
TouchSelectionMagnifierRunnerAsh* magnifier_runner = GetMagnifierRunner();
ASSERT_TRUE(magnifier_runner);
magnifier_runner->ShowMagnifier(GetContext(), gfx::PointF(300, 200));
ASSERT_TRUE(magnifier_runner->GetMagnifierLayerForTesting());
ASSERT_TRUE(magnifier_runner->GetZoomLayerForTesting());
magnifier_runner->CloseMagnifier();
RunPendingMessages();
EXPECT_FALSE(magnifier_runner->GetMagnifierLayerForTesting());
EXPECT_FALSE(magnifier_runner->GetZoomLayerForTesting());
}
// Tests that the magnifier layer is positioned with the correct bounds.
TEST_F(TouchSelectionMagnifierRunnerAshTest, LayerBounds) {
// Tests that the magnifier bounds are set correctly.
TEST_F(TouchSelectionMagnifierRunnerAshTest, CorrectBounds) {
TouchSelectionMagnifierRunnerAsh* magnifier_runner = GetMagnifierRunner();
ASSERT_TRUE(magnifier_runner);
gfx::PointF position(300, 200);
magnifier_runner->ShowMagnifier(GetContext(), position);
const ui::Layer* magnifier_layer =
magnifier_runner->GetMagnifierLayerForTesting();
ASSERT_TRUE(magnifier_layer);
gfx::Rect bounds = magnifier_layer->bounds();
EXPECT_EQ(bounds.size(),
TouchSelectionMagnifierRunnerAsh::kMagnifierLayerSize);
EXPECT_EQ(
bounds.CenterPoint(),
gfx::Point(
position.x(),
position.y() +
TouchSelectionMagnifierRunnerAsh::kMagnifierVerticalOffset));
magnifier_runner->CloseMagnifier();
RunPendingMessages();
}
// Tests that the magnifier layer bounds update correctly.
TEST_F(TouchSelectionMagnifierRunnerAshTest, LayerUpdatesBounds) {
TouchSelectionMagnifierRunnerAsh* magnifier_runner = GetMagnifierRunner();
ASSERT_TRUE(magnifier_runner);
gfx::PointF position(300, 200);
magnifier_runner->ShowMagnifier(GetContext(), position);
const ui::Layer* magnifier_layer =
magnifier_runner->GetMagnifierLayerForTesting();
ASSERT_TRUE(magnifier_layer);
gfx::Rect bounds = magnifier_layer->bounds();
EXPECT_EQ(bounds.size(),
TouchSelectionMagnifierRunnerAsh::kMagnifierLayerSize);
EXPECT_EQ(
bounds.CenterPoint(),
gfx::Point(
position.x(),
position.y() +
TouchSelectionMagnifierRunnerAsh::kMagnifierVerticalOffset));
VerifyMagnifierBounds(position);
// Move the magnifier.
position = gfx::PointF(400, 150);
magnifier_runner->ShowMagnifier(GetContext(), position);
EXPECT_EQ(magnifier_layer, magnifier_runner->GetMagnifierLayerForTesting());
bounds = magnifier_layer->bounds();
EXPECT_EQ(bounds.size(),
TouchSelectionMagnifierRunnerAsh::kMagnifierLayerSize);
EXPECT_EQ(
bounds.CenterPoint(),
gfx::Point(
position.x(),
position.y() +
TouchSelectionMagnifierRunnerAsh::kMagnifierVerticalOffset));
VerifyMagnifierBounds(position);
magnifier_runner->CloseMagnifier();
RunPendingMessages();