1 // Copyright 2020 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "ash/display/display_alignment_indicator.h"
7 #include "ash/public/cpp/shell_window_ids.h"
8 #include "ash/resources/vector_icons/vector_icons.h"
9 #include "ash/root_window_controller.h"
10 #include "ash/shell.h"
11 #include "base/memory/ptr_util.h"
12 #include "base/memory/raw_ptr.h"
13 #include "base/strings/utf_string_conversions.h"
14 #include "ui/compositor/layer.h"
15 #include "ui/display/display.h"
16 #include "ui/gfx/color_palette.h"
17 #include "ui/gfx/image/image_skia_operations.h"
18 #include "ui/gfx/paint_vector_icon.h"
19 #include "ui/gfx/vector_icon_types.h"
20 #include "ui/views/background.h"
21 #include "ui/views/controls/image_view.h"
22 #include "ui/views/controls/label.h"
23 #include "ui/views/view.h"
29 // Constants for indicator highlights.
30 constexpr SkColor kEdgeHighlightColor = gfx::kGoogleBlue600;
31 constexpr int kHighlightShadowElevation = 2;
33 // Thickness (and radius) of indicator highlight is dependent on resolution.
34 // If display has resolution smaller than 1440p, then its thickness is
35 // |kHighlightRadiusSub1440p|. Otherwise, use |kHighlightRadius1440p|.
36 constexpr int kHighlightRadiusSub1440p = 4;
37 constexpr int kHighlightRadius1440p = 6;
38 constexpr int kHighlightSizeChangeRes = 1440;
40 // Constants for pill theme.
41 // White with ~60% opacity.
42 constexpr SkColor kPillBackgroundColor = SkColorSetARGB(0x99, 0xFF, 0xFF, 0xFF);
43 constexpr SkColor kPillTextColor = gfx::kGoogleBlue600;
44 constexpr int kPillBackgroundBlur = 10;
46 // Constants for pill layout.
47 constexpr int kPillRadius = 12;
48 constexpr int kMaxPillWidth = 192;
49 constexpr int kPillHeight = 32;
50 constexpr int kTextMarginNormal = 24;
51 constexpr int kTextMarginElided = 20;
52 // Distance between the indicator highlight and the pill.
53 constexpr int kPillMargin = 20;
55 // Constants for arrow layout.
56 constexpr int kArrowSize = 28;
57 constexpr int kArrowHorizontalMargin = 12;
58 constexpr int kArrowVerticalMargin = (kPillHeight - kArrowSize) / 2;
59 constexpr int kArrowAllocatedWidth =
60 kArrowHorizontalMargin + kArrowSize + kArrowHorizontalMargin;
62 enum class IndicatorPosition { kTop, kRight, kBottom, kLeft };
64 // Returns IndicatorPosition given the bounds of an indicator highlight along
65 // with its corresponding display.
66 IndicatorPosition GetIndicatorPosition(const display::Display& src_display,
67 const gfx::Rect& indicator_bounds) {
68 const gfx::Point midpoint = src_display.bounds().CenterPoint();
70 // Horizontal shared edge (kTop or kBottom)
71 if (indicator_bounds.width() > indicator_bounds.height()) {
72 return (indicator_bounds.y() < midpoint.y()) ? IndicatorPosition::kTop
73 : IndicatorPosition::kBottom;
75 // Vertical shared edge (kLeft or kRight)
76 return (indicator_bounds.x() < midpoint.x()) ? IndicatorPosition::kLeft
77 : IndicatorPosition::kRight;
80 // Indicator thickness is dependent on display resolution.
81 int GetIndicatorThickness(const gfx::Size& display_size) {
82 return std::min(display_size.width(), display_size.height()) >
83 kHighlightSizeChangeRes
84 ? kHighlightRadius1440p
85 : kHighlightRadiusSub1440p;
88 // Adjust the indicator bounds to the correct thickness depending on the
89 // resolution of |display|.
90 void AdjustIndicatorBounds(const display::Display& display,
91 gfx::Rect* out_indicator_bounds) {
92 const int indicator_thickness = GetIndicatorThickness(display.size());
94 // Apply the new thickness to the indicator.
95 if (out_indicator_bounds->height() > out_indicator_bounds->width())
96 out_indicator_bounds->set_width(indicator_thickness);
98 out_indicator_bounds->set_height(indicator_thickness);
100 // Create enough space for the full indicator on the x and y axis.
101 const gfx::Point display_bottom_right = display.bounds().bottom_right();
102 if (out_indicator_bounds->x() == (display_bottom_right.x() - 1))
103 out_indicator_bounds->set_x(display_bottom_right.x() - indicator_thickness);
104 else if (out_indicator_bounds->y() == (display_bottom_right.y() - 1))
105 out_indicator_bounds->set_y(display_bottom_right.y() - indicator_thickness);
108 // Returns the pill's origin based on |pill_size| and the indicator's
109 // |thickened_bounds|.
110 gfx::Point GetPillOrigin(const gfx::Size& pill_size,
111 IndicatorPosition src_position,
112 const gfx::Rect& thickened_bounds) {
113 gfx::Point pill_origin;
114 switch (src_position) {
115 case IndicatorPosition::kLeft:
116 pill_origin = thickened_bounds.right_center();
117 pill_origin.Offset(kPillMargin, -1 * kPillHeight / 2);
119 case IndicatorPosition::kRight:
120 pill_origin = thickened_bounds.left_center();
121 pill_origin.Offset(-1 * kPillMargin - pill_size.width(),
122 -1 * kPillHeight / 2);
124 case IndicatorPosition::kTop:
125 pill_origin = thickened_bounds.bottom_center();
126 pill_origin.Offset(-1 * pill_size.width() / 2, kPillMargin);
128 case IndicatorPosition::kBottom:
129 pill_origin = thickened_bounds.top_center();
130 pill_origin.Offset(-1 * pill_size.width() / 2,
131 -1 * kPillMargin - kPillHeight);
138 views::Widget::InitParams CreateInitParams(int64_t display_id,
139 const std::string& target_name) {
140 views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP);
143 Shell::GetRootWindowControllerWithDisplayId(display_id)->GetRootWindow();
145 params.parent = Shell::GetContainer(root, kShellWindowId_OverlayContainer);
146 params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
148 views::Widget::InitParams::Ownership::WIDGET_OWNS_NATIVE_WIDGET;
149 params.activatable = views::Widget::InitParams::Activatable::kNo;
150 params.accept_events = false;
151 params.name = target_name;
158 // -----------------------------------------------------------------------------
159 // IndicatorHighlightView:
160 // View for the indicator highlight that renders on a shared edge of a given
162 class IndicatorHighlightView : public views::View {
164 explicit IndicatorHighlightView(const display::Display& display)
165 // Corner radius is the same as edge thickness.
166 : corner_radius_(GetIndicatorThickness(display.size())) {
167 SetPaintToLayer(ui::LAYER_TEXTURED);
169 layer()->SetFillsBoundsOpaquely(false);
170 layer()->SetIsFastRoundedCorner(true);
171 SetBackground(views::CreateSolidBackground(kEdgeHighlightColor));
174 IndicatorHighlightView(const IndicatorHighlightView&) = delete;
175 IndicatorHighlightView& operator=(const IndicatorHighlightView&) = delete;
176 ~IndicatorHighlightView() override = default;
178 // Sets which corners should be rounded depending on the position of the
180 void SetPosition(IndicatorPosition position) {
181 gfx::RoundedCornersF corners;
184 case IndicatorPosition::kLeft:
185 corners = {0, corner_radius_, corner_radius_, 0};
187 case IndicatorPosition::kRight:
188 corners = {corner_radius_, 0, 0, corner_radius_};
190 case IndicatorPosition::kTop:
191 corners = {0, 0, corner_radius_, corner_radius_};
193 case IndicatorPosition::kBottom:
194 corners = {corner_radius_, corner_radius_, 0, 0};
198 layer()->SetRoundedCornerRadius(corners);
202 const char* GetClassName() const override { return "IndicatorHighlightView"; }
205 // Radius for the rounded rectangle highlight. Determined by display
207 float corner_radius_;
210 // -----------------------------------------------------------------------------
211 // IndicatorPillView:
212 // View for the pill with an arrow pointing to an indicator highlight and name
213 // of the target display.
214 class IndicatorPillView : public views::View {
216 explicit IndicatorPillView(const std::u16string& text)
217 : // TODO(1070352): Replace current placeholder arrow in
219 icon_(AddChildView(std::make_unique<views::ImageView>())),
220 text_label_(AddChildView(std::make_unique<views::Label>())),
222 CreateVectorIcon(kLockScreenArrowIcon, gfx::kGoogleBlue600)) {
223 SetPaintToLayer(ui::LAYER_TEXTURED);
225 layer()->SetFillsBoundsOpaquely(false);
226 layer()->SetIsFastRoundedCorner(true);
227 layer()->SetBackgroundBlur(kPillBackgroundBlur);
228 layer()->SetRoundedCornerRadius(gfx::RoundedCornersF{kPillRadius});
231 views::CreateRoundedRectBackground(kPillBackgroundColor, kPillRadius));
233 text_label_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
234 text_label_->SetEnabledColor(kPillTextColor);
235 text_label_->SetElideBehavior(gfx::ElideBehavior::ELIDE_TAIL);
236 text_label_->SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_CENTER);
237 text_label_->SetVerticalAlignment(gfx::VerticalAlignment::ALIGN_MIDDLE);
239 text_label_->SetText(text);
241 icon_->SetImage(arrow_image_);
244 IndicatorPillView(const IndicatorPillView&) = delete;
245 IndicatorPillView& operator=(const IndicatorPillView&) = delete;
246 ~IndicatorPillView() override = default;
249 gfx::Size CalculatePreferredSize() const override {
250 // Pill is laid out as:
252 // Has max width of kMaxPillWidth.
254 const int text_width = text_label_->CalculatePreferredSize().width();
255 const int width = kArrowAllocatedWidth + text_width + kTextMarginNormal;
257 return gfx::Size(std::min(width, kMaxPillWidth), kPillHeight);
261 void Layout() override {
262 icon_->SetImageSize(gfx::Size(kArrowSize, kArrowSize));
264 // IndicatorPosition::kRight is a special case for layout as it is the only
265 // time where the arrow is on the right of the text instead of the usual
267 const int local_width = GetLocalBounds().width();
268 int icon_x = position_ == IndicatorPosition::kRight
269 ? local_width - kArrowHorizontalMargin - kArrowSize
270 : kArrowHorizontalMargin;
272 icon_->SetBoundsRect(
273 gfx::Rect(icon_x, kArrowVerticalMargin, kArrowSize, kArrowSize));
275 // If width of the pill is equal or greater than the max pill width, then
276 // text is elided and thus side margin must be reduced.
277 const int side_margin = CalculatePreferredSize().width() >= kMaxPillWidth
281 const int text_label_width =
282 local_width - kArrowAllocatedWidth - side_margin;
284 const int text_label_x = position_ == IndicatorPosition::kRight
286 : kArrowAllocatedWidth;
288 text_label_->SetBoundsRect(
289 gfx::Rect(text_label_x, 0, text_label_width, kPillHeight));
293 const char* GetClassName() const override { return "IndicatorPillView"; }
295 // Rotates the arrow depending on indicator highlight's position on-screen.
296 void SetPosition(IndicatorPosition position) {
297 if (position_ == position)
300 position_ = position;
303 case IndicatorPosition::kLeft:
304 icon_->SetImage(gfx::ImageSkiaOperations::CreateRotatedImage(
305 arrow_image_, SkBitmapOperations::ROTATION_180_CW));
307 case IndicatorPosition::kRight:
308 // |arrow_image_| points to right by default; no rotation required.
309 icon_->SetImage(arrow_image_);
311 case IndicatorPosition::kTop:
312 icon_->SetImage(gfx::ImageSkiaOperations::CreateRotatedImage(
313 arrow_image_, SkBitmapOperations::ROTATION_270_CW));
315 case IndicatorPosition::kBottom:
316 icon_->SetImage(gfx::ImageSkiaOperations::CreateRotatedImage(
317 arrow_image_, SkBitmapOperations::ROTATION_90_CW));
323 // Possibly rotated image of an arrow based on |vector_icon_|.
324 raw_ptr<views::ImageView, ExperimentalAsh> icon_ = nullptr; // NOT OWNED
325 // Label containing name of target display in the pill.
326 raw_ptr<views::Label, ExperimentalAsh> text_label_ = nullptr; // NOT OWNED
327 gfx::ImageSkia arrow_image_;
328 // The side of the display indicator is postioned on. Used to determine arrow
329 // direction and placement.
330 IndicatorPosition position_ = IndicatorPosition::kRight;
333 // -----------------------------------------------------------------------------
334 // DisplayAlignmentIndicator:
337 std::unique_ptr<DisplayAlignmentIndicator> DisplayAlignmentIndicator::Create(
338 const display::Display& src_display,
339 const gfx::Rect& bounds) {
340 // Using `new` to access a non-public constructor.
341 return base::WrapUnique(
342 new DisplayAlignmentIndicator(src_display, bounds, ""));
346 std::unique_ptr<DisplayAlignmentIndicator>
347 DisplayAlignmentIndicator::CreateWithPill(const display::Display& src_display,
348 const gfx::Rect& bounds,
349 const std::string& target_name) {
350 // Using `new` to access a non-public constructor.
351 return base::WrapUnique(
352 new DisplayAlignmentIndicator(src_display, bounds, target_name));
355 DisplayAlignmentIndicator::DisplayAlignmentIndicator(
356 const display::Display& src_display,
357 const gfx::Rect& bounds,
358 const std::string& target_name)
359 : display_id_(src_display.id()) {
360 gfx::Rect thickened_bounds = bounds;
361 AdjustIndicatorBounds(src_display, &thickened_bounds);
363 views::Widget::InitParams indicator_widget_params =
364 CreateInitParams(src_display.id(), "IndicatorHighlight");
365 indicator_widget_params.shadow_elevation = kHighlightShadowElevation;
367 indicator_widget_.Init(std::move(indicator_widget_params));
368 indicator_widget_.SetVisibilityChangedAnimationsEnabled(false);
369 indicator_view_ = indicator_widget_.SetContentsView(
370 std::make_unique<IndicatorHighlightView>(src_display));
371 indicator_widget_.SetBounds(thickened_bounds);
373 const IndicatorPosition indicator_position =
374 GetIndicatorPosition(src_display, thickened_bounds);
375 indicator_view_->SetPosition(indicator_position);
377 // Only create IndicatorPillView when |target_name| is specified.
378 if (!target_name.empty()) {
379 pill_widget_ = std::make_unique<views::Widget>();
380 pill_widget_->Init(CreateInitParams(src_display.id(), "IndicatorPill"));
381 pill_widget_->SetVisibilityChangedAnimationsEnabled(false);
382 pill_view_ = pill_widget_->SetContentsView(
383 std::make_unique<IndicatorPillView>(base::UTF8ToUTF16(target_name)));
384 pill_view_->SetPosition(indicator_position);
386 gfx::Size pill_size = pill_view_->GetPreferredSize();
387 gfx::Rect pill_bounds = gfx::Rect(
388 GetPillOrigin(pill_size, indicator_position, thickened_bounds),
390 pill_widget_->SetBounds(pill_bounds);
396 DisplayAlignmentIndicator::~DisplayAlignmentIndicator() = default;
398 void DisplayAlignmentIndicator::Show() {
399 indicator_widget_.Show();
402 pill_widget_->Show();
405 void DisplayAlignmentIndicator::Hide() {
406 indicator_widget_.Hide();
409 pill_widget_->Hide();
412 void DisplayAlignmentIndicator::Update(const display::Display& display,
414 DCHECK(!pill_widget_);
416 AdjustIndicatorBounds(display, &bounds);
417 const IndicatorPosition src_direction = GetIndicatorPosition(display, bounds);
418 indicator_view_->SetPosition(src_direction);
419 indicator_widget_.SetBounds(bounds);