1// Copyright 2013 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "ash/wm/header_painter.h"
6
7#include <vector>
8
9#include "ash/root_window_controller.h"
10#include "ash/wm/caption_buttons/frame_caption_button_container_view.h"
11#include "ash/wm/solo_window_tracker.h"
12#include "base/logging.h"  // DCHECK
13#include "grit/ash_resources.h"
14#include "third_party/skia/include/core/SkCanvas.h"
15#include "third_party/skia/include/core/SkColor.h"
16#include "third_party/skia/include/core/SkPaint.h"
17#include "third_party/skia/include/core/SkPath.h"
18#include "ui/aura/window.h"
19#include "ui/base/hit_test.h"
20#include "ui/base/resource/resource_bundle.h"
21#include "ui/base/theme_provider.h"
22#include "ui/gfx/animation/slide_animation.h"
23#include "ui/gfx/canvas.h"
24#include "ui/gfx/font.h"
25#include "ui/gfx/image/image.h"
26#include "ui/gfx/screen.h"
27#include "ui/gfx/skia_util.h"
28#include "ui/views/widget/widget.h"
29#include "ui/views/widget/widget_delegate.h"
30
31using aura::Window;
32using views::Widget;
33
34namespace {
35// Space between left edge of window and popup window icon.
36const int kIconOffsetX = 9;
37// Height and width of window icon.
38const int kIconSize = 16;
39// Space between the title text and the caption buttons.
40const int kTitleLogoSpacing = 5;
41// Space between window icon and title text.
42const int kTitleIconOffsetX = 5;
43// Space between window edge and title text, when there is no icon.
44const int kTitleNoIconOffsetX = 8;
45// Color for the non-maximized window title text.
46const SkColor kNonMaximizedWindowTitleTextColor = SkColorSetRGB(40, 40, 40);
47// Color for the maximized window title text.
48const SkColor kMaximizedWindowTitleTextColor = SK_ColorWHITE;
49// Size of header/content separator line below the header image.
50const int kHeaderContentSeparatorSize = 1;
51// Color of header bottom edge line.
52const SkColor kHeaderContentSeparatorColor = SkColorSetRGB(128, 128, 128);
53// In the pre-Ash era the web content area had a frame along the left edge, so
54// user-generated theme images for the new tab page assume they are shifted
55// right relative to the header.  Now that we have removed the left edge frame
56// we need to copy the theme image for the window header from a few pixels
57// inset to preserve alignment with the NTP image, or else we'll break a bunch
58// of existing themes.  We do something similar on OS X for the same reason.
59const int kThemeFrameImageInsetX = 5;
60// Duration of crossfade animation for activating and deactivating frame.
61const int kActivationCrossfadeDurationMs = 200;
62// Alpha/opacity value for fully-opaque headers.
63const int kFullyOpaque = 255;
64
65// Tiles an image into an area, rounding the top corners. Samples |image|
66// starting |image_inset_x| pixels from the left of the image.
67void TileRoundRect(gfx::Canvas* canvas,
68                   const gfx::ImageSkia& image,
69                   const SkPaint& paint,
70                   const gfx::Rect& bounds,
71                   int top_left_corner_radius,
72                   int top_right_corner_radius,
73                   int image_inset_x) {
74  SkRect rect = gfx::RectToSkRect(bounds);
75  const SkScalar kTopLeftRadius = SkIntToScalar(top_left_corner_radius);
76  const SkScalar kTopRightRadius = SkIntToScalar(top_right_corner_radius);
77  SkScalar radii[8] = {
78      kTopLeftRadius, kTopLeftRadius,  // top-left
79      kTopRightRadius, kTopRightRadius,  // top-right
80      0, 0,   // bottom-right
81      0, 0};  // bottom-left
82  SkPath path;
83  path.addRoundRect(rect, radii, SkPath::kCW_Direction);
84  canvas->DrawImageInPath(image, -image_inset_x, 0, path, paint);
85}
86
87// Tiles |frame_image| and |frame_overlay_image| into an area, rounding the top
88// corners.
89void PaintFrameImagesInRoundRect(gfx::Canvas* canvas,
90                                 const gfx::ImageSkia* frame_image,
91                                 const gfx::ImageSkia* frame_overlay_image,
92                                 const SkPaint& paint,
93                                 const gfx::Rect& bounds,
94                                 int corner_radius,
95                                 int image_inset_x) {
96  SkXfermode::Mode normal_mode;
97  SkXfermode::AsMode(NULL, &normal_mode);
98
99  // If |paint| is using an unusual SkXfermode::Mode (this is the case while
100  // crossfading), we must create a new canvas to overlay |frame_image| and
101  // |frame_overlay_image| using |normal_mode| and then paint the result
102  // using the unusual mode. We try to avoid this because creating a new
103  // browser-width canvas is expensive.
104  bool fast_path = (!frame_overlay_image ||
105      SkXfermode::IsMode(paint.getXfermode(), normal_mode));
106  if (fast_path) {
107    TileRoundRect(canvas, *frame_image, paint, bounds, corner_radius,
108        corner_radius, image_inset_x);
109
110    if (frame_overlay_image) {
111      // Adjust |bounds| such that |frame_overlay_image| is not tiled.
112      gfx::Rect overlay_bounds = bounds;
113      overlay_bounds.Intersect(
114          gfx::Rect(bounds.origin(), frame_overlay_image->size()));
115      int top_left_corner_radius = corner_radius;
116      int top_right_corner_radius = corner_radius;
117      if (overlay_bounds.width() < bounds.width() - corner_radius)
118        top_right_corner_radius = 0;
119      TileRoundRect(canvas, *frame_overlay_image, paint, overlay_bounds,
120          top_left_corner_radius, top_right_corner_radius, 0);
121    }
122  } else {
123    gfx::Canvas temporary_canvas(bounds.size(), canvas->image_scale(), false);
124    temporary_canvas.TileImageInt(*frame_image,
125                                  image_inset_x, 0,
126                                  0, 0,
127                                  bounds.width(), bounds.height());
128    temporary_canvas.DrawImageInt(*frame_overlay_image, 0, 0);
129    TileRoundRect(canvas, gfx::ImageSkia(temporary_canvas.ExtractImageRep()),
130        paint, bounds, corner_radius, corner_radius, 0);
131  }
132}
133
134}  // namespace
135
136namespace ash {
137
138// static
139int HeaderPainter::kActiveWindowOpacity = 255;  // 1.0
140int HeaderPainter::kInactiveWindowOpacity = 255;  // 1.0
141int HeaderPainter::kSoloWindowOpacity = 77;  // 0.3
142
143///////////////////////////////////////////////////////////////////////////////
144// HeaderPainter, public:
145
146HeaderPainter::HeaderPainter()
147    : frame_(NULL),
148      header_view_(NULL),
149      window_icon_(NULL),
150      caption_button_container_(NULL),
151      window_(NULL),
152      header_height_(0),
153      top_left_corner_(NULL),
154      top_edge_(NULL),
155      top_right_corner_(NULL),
156      header_left_edge_(NULL),
157      header_right_edge_(NULL),
158      previous_theme_frame_id_(0),
159      previous_theme_frame_overlay_id_(0),
160      previous_opacity_(0),
161      crossfade_theme_frame_id_(0),
162      crossfade_theme_frame_overlay_id_(0),
163      crossfade_opacity_(0) {}
164
165HeaderPainter::~HeaderPainter() {
166  // Sometimes we are destroyed before the window closes, so ensure we clean up.
167  if (window_)
168    window_->RemoveObserver(this);
169}
170
171void HeaderPainter::Init(
172    views::Widget* frame,
173    views::View* header_view,
174    views::View* window_icon,
175    FrameCaptionButtonContainerView* caption_button_container) {
176  DCHECK(frame);
177  DCHECK(header_view);
178  // window_icon may be NULL.
179  DCHECK(caption_button_container);
180  frame_ = frame;
181  header_view_ = header_view;
182  window_icon_ = window_icon;
183  caption_button_container_ = caption_button_container;
184
185  // Window frame image parts.
186  ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
187  top_left_corner_ =
188      rb.GetImageNamed(IDR_AURA_WINDOW_HEADER_SHADE_TOP_LEFT).ToImageSkia();
189  top_edge_ =
190      rb.GetImageNamed(IDR_AURA_WINDOW_HEADER_SHADE_TOP).ToImageSkia();
191  top_right_corner_ =
192      rb.GetImageNamed(IDR_AURA_WINDOW_HEADER_SHADE_TOP_RIGHT).ToImageSkia();
193  header_left_edge_ =
194      rb.GetImageNamed(IDR_AURA_WINDOW_HEADER_SHADE_LEFT).ToImageSkia();
195  header_right_edge_ =
196      rb.GetImageNamed(IDR_AURA_WINDOW_HEADER_SHADE_RIGHT).ToImageSkia();
197
198  window_ = frame->GetNativeWindow();
199
200  // Observer removes itself in OnWindowDestroying() below, or in the destructor
201  // if we go away before the window.
202  window_->AddObserver(this);
203
204  // Solo-window header updates are handled by the WorkspaceLayoutManager when
205  // this window is added to the desktop.
206}
207
208// static
209gfx::Rect HeaderPainter::GetBoundsForClientView(
210    int header_height,
211    const gfx::Rect& window_bounds) {
212  gfx::Rect client_bounds(window_bounds);
213  client_bounds.Inset(0, header_height, 0, 0);
214  return client_bounds;
215}
216
217// static
218gfx::Rect HeaderPainter::GetWindowBoundsForClientBounds(
219      int header_height,
220      const gfx::Rect& client_bounds) {
221  gfx::Rect window_bounds(client_bounds);
222  window_bounds.Inset(0, -header_height, 0, 0);
223  if (window_bounds.y() < 0)
224    window_bounds.set_y(0);
225  return window_bounds;
226}
227
228int HeaderPainter::NonClientHitTest(const gfx::Point& point) const {
229  gfx::Point point_in_header_view(point);
230  views::View::ConvertPointFromWidget(header_view_, &point_in_header_view);
231  if (!GetHeaderLocalBounds().Contains(point_in_header_view))
232    return HTNOWHERE;
233  if (caption_button_container_->visible()) {
234    gfx::Point point_in_caption_button_container(point);
235    views::View::ConvertPointFromWidget(caption_button_container_,
236        &point_in_caption_button_container);
237    int component = caption_button_container_->NonClientHitTest(
238        point_in_caption_button_container);
239    if (component != HTNOWHERE)
240      return component;
241  }
242  // Caption is a safe default.
243  return HTCAPTION;
244}
245
246int HeaderPainter::GetMinimumHeaderWidth() const {
247  // Ensure we have enough space for the window icon and buttons. We allow
248  // the title string to collapse to zero width.
249  return GetTitleOffsetX() +
250      caption_button_container_->GetMinimumSize().width();
251}
252
253int HeaderPainter::GetRightInset() const {
254  return caption_button_container_->GetPreferredSize().width();
255}
256
257int HeaderPainter::GetThemeBackgroundXInset() const {
258  return kThemeFrameImageInsetX;
259}
260
261void HeaderPainter::PaintHeader(gfx::Canvas* canvas,
262                                HeaderMode header_mode,
263                                int theme_frame_id,
264                                int theme_frame_overlay_id) {
265  bool initial_paint = (previous_theme_frame_id_ == 0);
266  if (!initial_paint &&
267      (previous_theme_frame_id_ != theme_frame_id ||
268       previous_theme_frame_overlay_id_ != theme_frame_overlay_id)) {
269    aura::Window* parent = frame_->GetNativeWindow()->parent();
270    // Don't animate the header if the parent (a workspace) is already
271    // animating. Doing so results in continually painting during the animation
272    // and gives a slower frame rate.
273    // TODO(sky): expose a better way to determine this rather than assuming
274    // the parent is a workspace.
275    bool parent_animating = parent &&
276        (parent->layer()->GetAnimator()->IsAnimatingProperty(
277            ui::LayerAnimationElement::OPACITY) ||
278         parent->layer()->GetAnimator()->IsAnimatingProperty(
279             ui::LayerAnimationElement::VISIBILITY));
280    if (!parent_animating) {
281      crossfade_animation_.reset(new gfx::SlideAnimation(this));
282      crossfade_theme_frame_id_ = previous_theme_frame_id_;
283      crossfade_theme_frame_overlay_id_ = previous_theme_frame_overlay_id_;
284      crossfade_opacity_ = previous_opacity_;
285      crossfade_animation_->SetSlideDuration(kActivationCrossfadeDurationMs);
286      crossfade_animation_->Show();
287    } else {
288      crossfade_animation_.reset();
289    }
290  }
291
292  int opacity =
293      GetHeaderOpacity(header_mode, theme_frame_id, theme_frame_overlay_id);
294  ui::ThemeProvider* theme_provider = frame_->GetThemeProvider();
295  gfx::ImageSkia* theme_frame = theme_provider->GetImageSkiaNamed(
296      theme_frame_id);
297  gfx::ImageSkia* theme_frame_overlay = NULL;
298  if (theme_frame_overlay_id != 0) {
299    theme_frame_overlay = theme_provider->GetImageSkiaNamed(
300        theme_frame_overlay_id);
301  }
302
303  int corner_radius = GetHeaderCornerRadius();
304  SkPaint paint;
305
306  if (crossfade_animation_.get() && crossfade_animation_->is_animating()) {
307    gfx::ImageSkia* crossfade_theme_frame =
308        theme_provider->GetImageSkiaNamed(crossfade_theme_frame_id_);
309    gfx::ImageSkia* crossfade_theme_frame_overlay = NULL;
310    if (crossfade_theme_frame_overlay_id_ != 0) {
311      crossfade_theme_frame_overlay = theme_provider->GetImageSkiaNamed(
312          crossfade_theme_frame_overlay_id_);
313    }
314    if (!crossfade_theme_frame ||
315        (crossfade_theme_frame_overlay_id_ != 0 &&
316         !crossfade_theme_frame_overlay)) {
317      // Reset the animation. This case occurs when the user switches the theme
318      // that they are using.
319      crossfade_animation_.reset();
320      paint.setAlpha(opacity);
321    } else {
322      double current_value = crossfade_animation_->GetCurrentValue();
323      int old_alpha = (1 - current_value) * crossfade_opacity_;
324      int new_alpha = current_value * opacity;
325
326      // Draw the old header background, clipping the corners to be rounded.
327      paint.setAlpha(old_alpha);
328      paint.setXfermodeMode(SkXfermode::kPlus_Mode);
329      PaintFrameImagesInRoundRect(canvas,
330                                  crossfade_theme_frame,
331                                  crossfade_theme_frame_overlay,
332                                  paint,
333                                  GetHeaderLocalBounds(),
334                                  corner_radius,
335                                  GetThemeBackgroundXInset());
336
337      paint.setAlpha(new_alpha);
338    }
339  } else {
340    paint.setAlpha(opacity);
341  }
342
343  // Draw the header background, clipping the corners to be rounded.
344  PaintFrameImagesInRoundRect(canvas,
345                              theme_frame,
346                              theme_frame_overlay,
347                              paint,
348                              GetHeaderLocalBounds(),
349                              corner_radius,
350                              GetThemeBackgroundXInset());
351
352  previous_theme_frame_id_ = theme_frame_id;
353  previous_theme_frame_overlay_id_ = theme_frame_overlay_id;
354  previous_opacity_ = opacity;
355
356  // We don't need the extra lightness in the edges when we're at the top edge
357  // of the screen or when the header's corners are not rounded.
358  //
359  // TODO(sky): this isn't quite right. What we really want is a method that
360  // returns bounds ignoring transforms on certain windows (such as workspaces)
361  // and is relative to the root.
362  if (frame_->GetNativeWindow()->bounds().y() == 0 || corner_radius == 0)
363    return;
364
365  // Draw the top corners and edge.
366  int top_left_width = top_left_corner_->width();
367  int top_left_height = top_left_corner_->height();
368  canvas->DrawImageInt(*top_left_corner_,
369                       0, 0, top_left_width, top_left_height,
370                       0, 0, top_left_width, top_left_height,
371                       false);
372  canvas->TileImageInt(*top_edge_,
373      top_left_width,
374      0,
375      header_view_->width() - top_left_width - top_right_corner_->width(),
376      top_edge_->height());
377  int top_right_height = top_right_corner_->height();
378  canvas->DrawImageInt(*top_right_corner_,
379                       0, 0,
380                       top_right_corner_->width(), top_right_height,
381                       header_view_->width() - top_right_corner_->width(), 0,
382                       top_right_corner_->width(), top_right_height,
383                       false);
384
385  // Header left edge.
386  int header_left_height = theme_frame->height() - top_left_height;
387  canvas->TileImageInt(*header_left_edge_,
388                       0, top_left_height,
389                       header_left_edge_->width(), header_left_height);
390
391  // Header right edge.
392  int header_right_height = theme_frame->height() - top_right_height;
393  canvas->TileImageInt(*header_right_edge_,
394                       header_view_->width() - header_right_edge_->width(),
395                       top_right_height,
396                       header_right_edge_->width(),
397                       header_right_height);
398
399  // We don't draw edges around the content area.  Web content goes flush
400  // to the edge of the window.
401}
402
403void HeaderPainter::PaintHeaderContentSeparator(gfx::Canvas* canvas) {
404  canvas->FillRect(gfx::Rect(0,
405                             header_height_ - kHeaderContentSeparatorSize,
406                             header_view_->width(),
407                             kHeaderContentSeparatorSize),
408                   kHeaderContentSeparatorColor);
409}
410
411int HeaderPainter::HeaderContentSeparatorSize() const {
412  return kHeaderContentSeparatorSize;
413}
414
415void HeaderPainter::PaintTitleBar(gfx::Canvas* canvas,
416                                  const gfx::Font& title_font) {
417  // The window icon is painted by its own views::View.
418  views::WidgetDelegate* delegate = frame_->widget_delegate();
419  if (delegate && delegate->ShouldShowWindowTitle()) {
420    gfx::Rect title_bounds = GetTitleBounds(title_font);
421    SkColor title_color = (frame_->IsMaximized() || frame_->IsFullscreen()) ?
422        kMaximizedWindowTitleTextColor : kNonMaximizedWindowTitleTextColor;
423    canvas->DrawStringInt(delegate->GetWindowTitle(),
424                          title_font,
425                          title_color,
426                          header_view_->GetMirroredXForRect(title_bounds),
427                          title_bounds.y(),
428                          title_bounds.width(),
429                          title_bounds.height(),
430                          gfx::Canvas::NO_SUBPIXEL_RENDERING);
431  }
432}
433
434void HeaderPainter::LayoutHeader(bool shorter_layout) {
435  caption_button_container_->set_header_style(shorter_layout ?
436      FrameCaptionButtonContainerView::HEADER_STYLE_SHORT :
437      FrameCaptionButtonContainerView::HEADER_STYLE_TALL);
438  caption_button_container_->Layout();
439
440  gfx::Size caption_button_container_size =
441      caption_button_container_->GetPreferredSize();
442  caption_button_container_->SetBounds(
443      header_view_->width() - caption_button_container_size.width(),
444      0,
445      caption_button_container_size.width(),
446      caption_button_container_size.height());
447
448  if (window_icon_) {
449    // Vertically center the window icon with respect to the caption button
450    // container.
451    int icon_offset_y =
452        GetCaptionButtonContainerCenterY() - window_icon_->height() / 2;
453    window_icon_->SetBounds(kIconOffsetX, icon_offset_y, kIconSize, kIconSize);
454  }
455}
456
457void HeaderPainter::SchedulePaintForTitle(const gfx::Font& title_font) {
458  header_view_->SchedulePaintInRect(GetTitleBounds(title_font));
459}
460
461void HeaderPainter::OnThemeChanged() {
462  // We do not cache the images for |previous_theme_frame_id_| and
463  // |previous_theme_frame_overlay_id_|. Changing the theme changes the images
464  // returned from ui::ThemeProvider for |previous_theme_frame_id_|
465  // and |previous_theme_frame_overlay_id_|. Reset the image ids to prevent
466  // starting a crossfade animation with these images.
467  previous_theme_frame_id_ = 0;
468  previous_theme_frame_overlay_id_ = 0;
469
470  if (crossfade_animation_.get() && crossfade_animation_->is_animating()) {
471    crossfade_animation_.reset();
472    header_view_->SchedulePaintInRect(GetHeaderLocalBounds());
473  }
474}
475
476///////////////////////////////////////////////////////////////////////////////
477// aura::WindowObserver overrides:
478
479void HeaderPainter::OnWindowDestroying(aura::Window* destroying) {
480  DCHECK_EQ(window_, destroying);
481
482  // Must be removed here and not in the destructor, as the aura::Window is
483  // already destroyed when our destructor runs.
484  window_->RemoveObserver(this);
485
486  window_ = NULL;
487}
488
489void HeaderPainter::OnWindowBoundsChanged(aura::Window* window,
490                                         const gfx::Rect& old_bounds,
491                                         const gfx::Rect& new_bounds) {
492  // TODO(sky): this isn't quite right. What we really want is a method that
493  // returns bounds ignoring transforms on certain windows (such as workspaces).
494  if ((!frame_->IsMaximized() && !frame_->IsFullscreen()) &&
495      ((old_bounds.y() == 0 && new_bounds.y() != 0) ||
496       (old_bounds.y() != 0 && new_bounds.y() == 0))) {
497    SchedulePaintForHeader();
498  }
499}
500
501///////////////////////////////////////////////////////////////////////////////
502// gfx::AnimationDelegate overrides:
503
504void HeaderPainter::AnimationProgressed(const gfx::Animation* animation) {
505  header_view_->SchedulePaintInRect(GetHeaderLocalBounds());
506}
507
508///////////////////////////////////////////////////////////////////////////////
509// HeaderPainter, private:
510
511gfx::Rect HeaderPainter::GetHeaderLocalBounds() const {
512  return gfx::Rect(header_view_->width(), header_height_);
513}
514
515int HeaderPainter::GetTitleOffsetX() const {
516  return window_icon_ ?
517      window_icon_->bounds().right() + kTitleIconOffsetX :
518      kTitleNoIconOffsetX;
519}
520
521int HeaderPainter::GetCaptionButtonContainerCenterY() const {
522  return caption_button_container_->y() +
523      caption_button_container_->height() / 2;
524}
525
526int HeaderPainter::GetHeaderCornerRadius() const {
527  bool square_corners = (frame_->IsMaximized() || frame_->IsFullscreen());
528  const int kCornerRadius = 2;
529  return square_corners ? 0 : kCornerRadius;
530}
531
532int HeaderPainter::GetHeaderOpacity(
533    HeaderMode header_mode,
534    int theme_frame_id,
535    int theme_frame_overlay_id) const {
536  // User-provided themes are painted fully opaque.
537  ui::ThemeProvider* theme_provider = frame_->GetThemeProvider();
538  if (theme_provider->HasCustomImage(theme_frame_id) ||
539      (theme_frame_overlay_id != 0 &&
540       theme_provider->HasCustomImage(theme_frame_overlay_id))) {
541    return kFullyOpaque;
542  }
543
544  // Maximized and fullscreen windows are fully opaque.
545  if (frame_->IsMaximized() || frame_->IsFullscreen())
546    return kFullyOpaque;
547
548  // Solo header is very transparent.
549  ash::SoloWindowTracker* solo_window_tracker =
550      internal::RootWindowController::ForWindow(window_)->solo_window_tracker();
551  if (solo_window_tracker &&
552      solo_window_tracker->GetWindowWithSoloHeader() == window_) {
553    return kSoloWindowOpacity;
554  }
555
556  // Otherwise, change transparency based on window activation status.
557  if (header_mode == ACTIVE)
558    return kActiveWindowOpacity;
559  return kInactiveWindowOpacity;
560}
561
562void HeaderPainter::SchedulePaintForHeader() {
563  int top_left_height = top_left_corner_->height();
564  int top_right_height = top_right_corner_->height();
565  header_view_->SchedulePaintInRect(
566      gfx::Rect(0, 0, header_view_->width(),
567                std::max(top_left_height, top_right_height)));
568}
569
570gfx::Rect HeaderPainter::GetTitleBounds(const gfx::Font& title_font) {
571  int title_x = GetTitleOffsetX();
572  // Center the text with respect to the caption button container. This way it
573  // adapts to the caption button height and aligns exactly with the window
574  // icon. Don't use |window_icon_| for this computation as it may be NULL.
575  int title_y = GetCaptionButtonContainerCenterY() - title_font.GetHeight() / 2;
576  return gfx::Rect(
577      title_x,
578      std::max(0, title_y),
579      std::max(0, caption_button_container_->x() - kTitleLogoSpacing - title_x),
580      title_font.GetHeight());
581}
582
583}  // namespace ash
584