// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "ui/views/bubble/bubble_border.h" #include // for std::max #include "base/logging.h" #include "grit/ui_resources.h" #include "grit/ui_resources_standard.h" #include "third_party/skia/include/core/SkBitmap.h" #include "ui/base/resource/resource_bundle.h" #include "ui/gfx/canvas.h" #include "ui/gfx/skia_util.h" namespace views { struct BubbleBorder::BorderImages { BorderImages() : left(NULL), top_left(NULL), top(NULL), top_right(NULL), right(NULL), bottom_right(NULL), bottom(NULL), bottom_left(NULL), left_arrow(NULL), top_arrow(NULL), right_arrow(NULL), bottom_arrow(NULL), border_thickness(0) { } SkBitmap* left; SkBitmap* top_left; SkBitmap* top; SkBitmap* top_right; SkBitmap* right; SkBitmap* bottom_right; SkBitmap* bottom; SkBitmap* bottom_left; SkBitmap* left_arrow; SkBitmap* top_arrow; SkBitmap* right_arrow; SkBitmap* bottom_arrow; int border_thickness; }; // static struct BubbleBorder::BorderImages* BubbleBorder::normal_images_ = NULL; struct BubbleBorder::BorderImages* BubbleBorder::shadow_images_ = NULL; // The height inside the arrow image, in pixels. static const int kArrowInteriorHeight = 7; BubbleBorder::BubbleBorder(ArrowLocation arrow_location, Shadow shadow) : override_arrow_offset_(0), arrow_location_(arrow_location), alignment_(ALIGN_ARROW_TO_MID_ANCHOR), background_color_(SK_ColorWHITE) { images_ = GetBorderImages(shadow); // Calculate horizontal and vertical insets for arrow by ensuring that // the widest arrow and corner images will have enough room to avoid overlap int offset_x = (std::max(images_->top_arrow->width(), images_->bottom_arrow->width()) / 2) + std::max(std::max(images_->top_left->width(), images_->top_right->width()), std::max(images_->bottom_left->width(), images_->bottom_right->width())); int offset_y = (std::max(images_->left_arrow->height(), images_->right_arrow->height()) / 2) + std::max(std::max(images_->top_left->height(), images_->top_right->height()), std::max(images_->bottom_left->height(), images_->bottom_right->height())); arrow_offset_ = std::max(offset_x, offset_y); } gfx::Rect BubbleBorder::GetBounds(const gfx::Rect& position_relative_to, const gfx::Size& contents_size) const { // Desired size is size of contents enlarged by the size of the border images. gfx::Size border_size(contents_size); gfx::Insets insets; GetInsets(&insets); border_size.Enlarge(insets.width(), insets.height()); // Screen position depends on the arrow location. // The arrow should overlap the target by some amount since there is space // for shadow between arrow tip and bitmap bounds. const int kArrowOverlap = 3; int x = position_relative_to.x(); int y = position_relative_to.y(); int w = position_relative_to.width(); int h = position_relative_to.height(); int arrow_offset = override_arrow_offset_ ? override_arrow_offset_ : arrow_offset_; // Calculate bubble x coordinate. switch (arrow_location_) { case TOP_LEFT: case BOTTOM_LEFT: x += alignment_ == ALIGN_ARROW_TO_MID_ANCHOR ? w / 2 - arrow_offset : -kArrowOverlap; break; case TOP_RIGHT: case BOTTOM_RIGHT: x += alignment_ == ALIGN_ARROW_TO_MID_ANCHOR ? w / 2 + arrow_offset - border_size.width() + 1 : w - border_size.width() + kArrowOverlap; break; case LEFT_TOP: case LEFT_BOTTOM: x += w - kArrowOverlap; break; case RIGHT_TOP: case RIGHT_BOTTOM: x += kArrowOverlap - border_size.width(); break; case NONE: case FLOAT: x += w / 2 - border_size.width() / 2; break; } // Calculate bubble y coordinate. switch (arrow_location_) { case TOP_LEFT: case TOP_RIGHT: y += h - kArrowOverlap; break; case BOTTOM_LEFT: case BOTTOM_RIGHT: y += kArrowOverlap - border_size.height(); break; case LEFT_TOP: case RIGHT_TOP: y += alignment_ == ALIGN_ARROW_TO_MID_ANCHOR ? h / 2 - arrow_offset : -kArrowOverlap; break; case LEFT_BOTTOM: case RIGHT_BOTTOM: y += alignment_ == ALIGN_ARROW_TO_MID_ANCHOR ? h / 2 + arrow_offset - border_size.height() + 1 : h - border_size.height() + kArrowOverlap; break; case NONE: y += h; break; case FLOAT: y += h / 2 - border_size.height() / 2; break; } return gfx::Rect(x, y, border_size.width(), border_size.height()); } void BubbleBorder::GetInsets(gfx::Insets* insets) const { int top = images_->top->height(); int bottom = images_->bottom->height(); int left = images_->left->width(); int right = images_->right->width(); switch (arrow_location_) { case TOP_LEFT: case TOP_RIGHT: top = std::max(top, images_->top_arrow->height()); break; case BOTTOM_LEFT: case BOTTOM_RIGHT: bottom = std::max(bottom, images_->bottom_arrow->height()); break; case LEFT_TOP: case LEFT_BOTTOM: left = std::max(left, images_->left_arrow->width()); break; case RIGHT_TOP: case RIGHT_BOTTOM: right = std::max(right, images_->right_arrow->width()); break; case NONE: case FLOAT: // Nothing to do. break; } insets->Set(top, left, bottom, right); } int BubbleBorder::border_thickness() const { return images_->border_thickness; } int BubbleBorder::SetArrowOffset(int offset, const gfx::Size& contents_size) { gfx::Size border_size(contents_size); gfx::Insets insets; GetInsets(&insets); border_size.Enlarge(insets.left() + insets.right(), insets.top() + insets.bottom()); offset = std::max(arrow_offset_, std::min(offset, (is_arrow_on_horizontal(arrow_location_) ? border_size.width() : border_size.height()) - arrow_offset_)); override_arrow_offset_ = offset; return override_arrow_offset_; } // static BubbleBorder::BorderImages* BubbleBorder::GetBorderImages(Shadow shadow) { if (shadow == SHADOW && shadow_images_ == NULL) { ResourceBundle& rb = ResourceBundle::GetSharedInstance(); shadow_images_ = new BorderImages(); shadow_images_->left = rb.GetBitmapNamed(IDR_BUBBLE_SHADOW_L); shadow_images_->top_left = rb.GetBitmapNamed(IDR_BUBBLE_SHADOW_TL); shadow_images_->top = rb.GetBitmapNamed(IDR_BUBBLE_SHADOW_T); shadow_images_->top_right = rb.GetBitmapNamed(IDR_BUBBLE_SHADOW_TR); shadow_images_->right = rb.GetBitmapNamed(IDR_BUBBLE_SHADOW_R); shadow_images_->bottom_right = rb.GetBitmapNamed(IDR_BUBBLE_SHADOW_BR); shadow_images_->bottom = rb.GetBitmapNamed(IDR_BUBBLE_SHADOW_B); shadow_images_->bottom_left = rb.GetBitmapNamed(IDR_BUBBLE_SHADOW_BL); shadow_images_->left_arrow = new SkBitmap(); shadow_images_->top_arrow = new SkBitmap(); shadow_images_->right_arrow = new SkBitmap(); shadow_images_->bottom_arrow = new SkBitmap(); shadow_images_->border_thickness = 10; } else if (shadow == NO_SHADOW && normal_images_ == NULL) { ResourceBundle& rb = ResourceBundle::GetSharedInstance(); normal_images_ = new BorderImages(); normal_images_->left = rb.GetBitmapNamed(IDR_BUBBLE_L); normal_images_->top_left = rb.GetBitmapNamed(IDR_BUBBLE_TL); normal_images_->top = rb.GetBitmapNamed(IDR_BUBBLE_T); normal_images_->top_right = rb.GetBitmapNamed(IDR_BUBBLE_TR); normal_images_->right = rb.GetBitmapNamed(IDR_BUBBLE_R); normal_images_->bottom_right = rb.GetBitmapNamed(IDR_BUBBLE_BR); normal_images_->bottom = rb.GetBitmapNamed(IDR_BUBBLE_B); normal_images_->bottom_left = rb.GetBitmapNamed(IDR_BUBBLE_BL); normal_images_->left_arrow = rb.GetBitmapNamed(IDR_BUBBLE_L_ARROW); normal_images_->top_arrow = rb.GetBitmapNamed(IDR_BUBBLE_T_ARROW); normal_images_->right_arrow = rb.GetBitmapNamed(IDR_BUBBLE_R_ARROW); normal_images_->bottom_arrow = rb.GetBitmapNamed(IDR_BUBBLE_B_ARROW); normal_images_->border_thickness = 0; } return shadow == SHADOW ? shadow_images_ : normal_images_; } BubbleBorder::~BubbleBorder() {} void BubbleBorder::Paint(const views::View& view, gfx::Canvas* canvas) const { // Convenience shorthand variables. const int tl_width = images_->top_left->width(); const int tl_height = images_->top_left->height(); const int t_height = images_->top->height(); const int tr_width = images_->top_right->width(); const int tr_height = images_->top_right->height(); const int l_width = images_->left->width(); const int r_width = images_->right->width(); const int br_width = images_->bottom_right->width(); const int br_height = images_->bottom_right->height(); const int b_height = images_->bottom->height(); const int bl_width = images_->bottom_left->width(); const int bl_height = images_->bottom_left->height(); gfx::Insets insets; GetInsets(&insets); const int top = insets.top() - t_height; const int bottom = view.height() - insets.bottom() + b_height; const int left = insets.left() - l_width; const int right = view.width() - insets.right() + r_width; const int height = bottom - top; const int width = right - left; // |arrow_offset| is offset of arrow from the begining of the edge. int arrow_offset = arrow_offset_; if (override_arrow_offset_) arrow_offset = override_arrow_offset_; else if (is_arrow_on_horizontal(arrow_location_) && !is_arrow_on_left(arrow_location_)) { arrow_offset = view.width() - arrow_offset - 1; } else if (!is_arrow_on_horizontal(arrow_location_) && !is_arrow_on_top(arrow_location_)) { arrow_offset = view.height() - arrow_offset - 1; } // Left edge. if (arrow_location_ == LEFT_TOP || arrow_location_ == LEFT_BOTTOM) { int start_y = top + tl_height; int before_arrow = arrow_offset - start_y - images_->left_arrow->height() / 2; int after_arrow = height - tl_height - bl_height - images_->left_arrow->height() - before_arrow; int tip_y = start_y + before_arrow + images_->left_arrow->height() / 2; DrawArrowInterior(canvas, false, images_->left_arrow->width() - kArrowInteriorHeight, tip_y, kArrowInteriorHeight, images_->left_arrow->height() / 2 - 1); DrawEdgeWithArrow(canvas, false, images_->left, images_->left_arrow, left, start_y, before_arrow, after_arrow, images_->left->width() - images_->left_arrow->width()); } else { canvas->TileImageInt(*images_->left, left, top + tl_height, l_width, height - tl_height - bl_height); } // Top left corner. canvas->DrawBitmapInt(*images_->top_left, left, top); // Top edge. if (arrow_location_ == TOP_LEFT || arrow_location_ == TOP_RIGHT) { int start_x = left + tl_width; int before_arrow = arrow_offset - start_x - images_->top_arrow->width() / 2; int after_arrow = width - tl_width - tr_width - images_->top_arrow->width() - before_arrow; DrawArrowInterior(canvas, true, start_x + before_arrow + images_->top_arrow->width() / 2, images_->top_arrow->height() - kArrowInteriorHeight, 1 - images_->top_arrow->width() / 2, kArrowInteriorHeight); DrawEdgeWithArrow(canvas, true, images_->top, images_->top_arrow, start_x, top, before_arrow, after_arrow, images_->top->height() - images_->top_arrow->height()); } else { canvas->TileImageInt(*images_->top, left + tl_width, top, width - tl_width - tr_width, t_height); } // Top right corner. canvas->DrawBitmapInt(*images_->top_right, right - tr_width, top); // Right edge. if (arrow_location_ == RIGHT_TOP || arrow_location_ == RIGHT_BOTTOM) { int start_y = top + tr_height; int before_arrow = arrow_offset - start_y - images_->right_arrow->height() / 2; int after_arrow = height - tl_height - bl_height - images_->right_arrow->height() - before_arrow; int tip_y = start_y + before_arrow + images_->right_arrow->height() / 2; DrawArrowInterior(canvas, false, right - r_width + kArrowInteriorHeight, tip_y, -kArrowInteriorHeight, images_->right_arrow->height() / 2 - 1); DrawEdgeWithArrow(canvas, false, images_->right, images_->right_arrow, right - r_width, start_y, before_arrow, after_arrow, 0); } else { canvas->TileImageInt(*images_->right, right - r_width, top + tr_height, r_width, height - tr_height - br_height); } // Bottom right corner. canvas->DrawBitmapInt(*images_->bottom_right, right - br_width, bottom - br_height); // Bottom edge. if (arrow_location_ == BOTTOM_LEFT || arrow_location_ == BOTTOM_RIGHT) { int start_x = left + bl_width; int before_arrow = arrow_offset - start_x - images_->bottom_arrow->width() / 2; int after_arrow = width - bl_width - br_width - images_->bottom_arrow->width() - before_arrow; int tip_x = start_x + before_arrow + images_->bottom_arrow->width() / 2; DrawArrowInterior(canvas, true, tip_x, bottom - b_height + kArrowInteriorHeight, 1 - images_->bottom_arrow->width() / 2, -kArrowInteriorHeight); DrawEdgeWithArrow(canvas, true, images_->bottom, images_->bottom_arrow, start_x, bottom - b_height, before_arrow, after_arrow, 0); } else { canvas->TileImageInt(*images_->bottom, left + bl_width, bottom - b_height, width - bl_width - br_width, b_height); } // Bottom left corner. canvas->DrawBitmapInt(*images_->bottom_left, left, bottom - bl_height); } void BubbleBorder::DrawEdgeWithArrow(gfx::Canvas* canvas, bool is_horizontal, SkBitmap* edge, SkBitmap* arrow, int start_x, int start_y, int before_arrow, int after_arrow, int offset) const { /* Here's what the parameters mean: * start_x * . * . ┌───┐ ┬ offset * start_y..........┌────┬────────┤ ▲ ├────────┬────┐ * │ / │--------│∙ ∙│--------│ \ │ * │ / ├────────┴───┴────────┤ \ │ * ├───┬┘ └┬───┤ * └───┬────┘ └───┬────┘ * before_arrow ─┘ └─ after_arrow */ if (before_arrow) { canvas->TileImageInt(*edge, start_x, start_y, is_horizontal ? before_arrow : edge->width(), is_horizontal ? edge->height() : before_arrow); } canvas->DrawBitmapInt(*arrow, start_x + (is_horizontal ? before_arrow : offset), start_y + (is_horizontal ? offset : before_arrow)); if (after_arrow) { start_x += (is_horizontal ? before_arrow + arrow->width() : 0); start_y += (is_horizontal ? 0 : before_arrow + arrow->height()); canvas->TileImageInt(*edge, start_x, start_y, is_horizontal ? after_arrow : edge->width(), is_horizontal ? edge->height() : after_arrow); } } void BubbleBorder::DrawArrowInterior(gfx::Canvas* canvas, bool is_horizontal, int tip_x, int tip_y, int shift_x, int shift_y) const { /* This function fills the interior of the arrow with background color. * It draws isosceles triangle under semitransparent arrow tip. * * Here's what the parameters mean: * * ┌──────── |tip_x| * ┌─────┐ * │ ▲ │ ──── |tip y| * │∙∙∙∙∙│ ┐ * └─────┘ └─── |shift_x| (offset from tip to vertexes of isosceles triangle) * └────────── |shift_y| */ SkPaint paint; paint.setStyle(SkPaint::kFill_Style); paint.setColor(background_color_); SkPath path; path.incReserve(4); path.moveTo(SkIntToScalar(tip_x), SkIntToScalar(tip_y)); path.lineTo(SkIntToScalar(tip_x + shift_x), SkIntToScalar(tip_y + shift_y)); if (is_horizontal) path.lineTo(SkIntToScalar(tip_x - shift_x), SkIntToScalar(tip_y + shift_y)); else path.lineTo(SkIntToScalar(tip_x + shift_x), SkIntToScalar(tip_y - shift_y)); path.close(); canvas->DrawPath(path, paint); } ///////////////////////// void BubbleBackground::Paint(gfx::Canvas* canvas, views::View* view) const { // Clip out the client bounds to prevent overlapping transparent widgets. if (!border_->client_bounds().IsEmpty()) { SkRect client_rect(gfx::RectToSkRect(border_->client_bounds())); canvas->sk_canvas()->clipRect(client_rect, SkRegion::kDifference_Op); } // The border of this view creates an anti-aliased round-rect region for the // contents, which we need to fill with the background color. // NOTE: This doesn't handle an arrow location of "NONE", which has square top // corners. SkPaint paint; paint.setAntiAlias(true); paint.setStyle(SkPaint::kFill_Style); paint.setColor(border_->background_color()); SkPath path; gfx::Rect bounds(view->GetContentsBounds()); bounds.Inset(-border_->border_thickness(), -border_->border_thickness()); SkScalar radius = SkIntToScalar(BubbleBorder::GetCornerRadius()); path.addRoundRect(gfx::RectToSkRect(bounds), radius, radius); canvas->DrawPath(path, paint); } } // namespace views