blob: ded87a9b3388a20c9e14db732ec1e9ae75a08e77 [file] [log] [blame]
// 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 "chrome/browser/ui/gtk/rounded_window.h"
#include <gtk/gtk.h>
#include <math.h>
#include "base/i18n/rtl.h"
#include "base/logging.h"
#include "chrome/browser/ui/gtk/gtk_util.h"
#include "ui/base/gtk/gtk_signal_registrar.h"
#include "ui/gfx/gtk_compat.h"
namespace gtk_util {
namespace {
const char* kRoundedData = "rounded-window-data";
// If the border radius is less than |kMinRoundedBorderSize|, we don't actually
// round the corners, we just truncate the corner.
const int kMinRoundedBorderSize = 8;
struct RoundedWindowData {
// Expected window size. Used to detect when we need to reshape the window.
int expected_width;
int expected_height;
// Color of the border.
GdkColor border_color;
// Radius of the edges in pixels.
int corner_size;
// Which corners should be rounded?
int rounded_edges;
// Which sides of the window should have an internal border?
int drawn_borders;
// Keeps track of attached signal handlers.
ui::GtkSignalRegistrar signals;
};
// Callback from GTK to release allocated memory.
void FreeRoundedWindowData(gpointer data) {
delete static_cast<RoundedWindowData*>(data);
}
enum FrameType {
FRAME_MASK,
FRAME_STROKE,
};
// Returns a list of points that either form the outline of the status bubble
// (|type| == FRAME_MASK) or form the inner border around the inner edge
// (|type| == FRAME_STROKE).
std::vector<GdkPoint> MakeFramePolygonPoints(RoundedWindowData* data,
FrameType type) {
using gtk_util::MakeBidiGdkPoint;
int width = data->expected_width;
int height = data->expected_height;
int corner_size = data->corner_size;
std::vector<GdkPoint> points;
bool ltr = !base::i18n::IsRTL();
// If we have a stroke, we have to offset some of our points by 1 pixel.
// We have to inset by 1 pixel when we draw horizontal lines that are on the
// bottom or when we draw vertical lines that are closer to the end (end is
// right for ltr).
int y_off = (type == FRAME_MASK) ? 0 : -1;
// We use this one for LTR.
int x_off_l = ltr ? y_off : 0;
// We use this one for RTL.
int x_off_r = !ltr ? -y_off : 0;
// Build up points starting with the bottom left corner and continuing
// clockwise.
// Bottom left corner.
if (type == FRAME_MASK ||
(data->drawn_borders & (BORDER_LEFT | BORDER_BOTTOM))) {
if (data->rounded_edges & ROUNDED_BOTTOM_LEFT) {
if (corner_size >= kMinRoundedBorderSize) {
// We are careful to only add points that are horizontal or vertically
// offset from the previous point (not both). This avoids rounding
// differences when two points are connected.
for (int x = 0; x <= corner_size; ++x) {
int y = static_cast<int>(sqrt(static_cast<double>(
(corner_size * corner_size) - (x * x))));
if (x > 0) {
points.push_back(MakeBidiGdkPoint(
corner_size - x + x_off_r + 1,
height - (corner_size - y) + y_off, width, ltr));
}
points.push_back(MakeBidiGdkPoint(
corner_size - x + x_off_r,
height - (corner_size - y) + y_off, width, ltr));
}
} else {
points.push_back(MakeBidiGdkPoint(
corner_size + x_off_l, height + y_off, width, ltr));
points.push_back(MakeBidiGdkPoint(
x_off_r, height - corner_size, width, ltr));
}
} else {
points.push_back(MakeBidiGdkPoint(x_off_r, height + y_off, width, ltr));
}
}
// Top left corner.
if (type == FRAME_MASK ||
(data->drawn_borders & (BORDER_LEFT | BORDER_TOP))) {
if (data->rounded_edges & ROUNDED_TOP_LEFT) {
if (corner_size >= kMinRoundedBorderSize) {
for (int x = corner_size; x >= 0; --x) {
int y = static_cast<int>(sqrt(static_cast<double>(
(corner_size * corner_size) - (x * x))));
points.push_back(MakeBidiGdkPoint(corner_size - x + x_off_r,
corner_size - y, width, ltr));
if (x > 0) {
points.push_back(MakeBidiGdkPoint(corner_size - x + 1 + x_off_r,
corner_size - y, width, ltr));
}
}
} else {
points.push_back(MakeBidiGdkPoint(
x_off_r, corner_size - 1, width, ltr));
points.push_back(MakeBidiGdkPoint(
corner_size + x_off_r - 1, 0, width, ltr));
}
} else {
points.push_back(MakeBidiGdkPoint(x_off_r, 0, width, ltr));
}
}
// Top right corner.
if (type == FRAME_MASK ||
(data->drawn_borders & (BORDER_TOP | BORDER_RIGHT))) {
if (data->rounded_edges & ROUNDED_TOP_RIGHT) {
if (corner_size >= kMinRoundedBorderSize) {
for (int x = 0; x <= corner_size; ++x) {
int y = static_cast<int>(sqrt(static_cast<double>(
(corner_size * corner_size) - (x * x))));
if (x > 0) {
points.push_back(MakeBidiGdkPoint(
width - (corner_size - x) + x_off_l - 1,
corner_size - y, width, ltr));
}
points.push_back(MakeBidiGdkPoint(
width - (corner_size - x) + x_off_l,
corner_size - y, width, ltr));
}
} else {
points.push_back(MakeBidiGdkPoint(
width - corner_size + 1 + x_off_l, 0, width, ltr));
points.push_back(MakeBidiGdkPoint(
width + x_off_l, corner_size - 1, width, ltr));
}
} else {
points.push_back(MakeBidiGdkPoint(
width + x_off_l, 0, width, ltr));
}
}
// Bottom right corner.
if (type == FRAME_MASK ||
(data->drawn_borders & (BORDER_RIGHT | BORDER_BOTTOM))) {
if (data->rounded_edges & ROUNDED_BOTTOM_RIGHT) {
if (corner_size >= kMinRoundedBorderSize) {
for (int x = corner_size; x >= 0; --x) {
int y = static_cast<int>(sqrt(static_cast<double>(
(corner_size * corner_size) - (x * x))));
points.push_back(MakeBidiGdkPoint(
width - (corner_size - x) + x_off_l,
height - (corner_size - y) + y_off, width, ltr));
if (x > 0) {
points.push_back(MakeBidiGdkPoint(
width - (corner_size - x) + x_off_l - 1,
height - (corner_size - y) + y_off, width, ltr));
}
}
} else {
points.push_back(MakeBidiGdkPoint(
width + x_off_l, height - corner_size, width, ltr));
points.push_back(MakeBidiGdkPoint(
width - corner_size + x_off_r, height + y_off, width, ltr));
}
} else {
points.push_back(MakeBidiGdkPoint(
width + x_off_l, height + y_off, width, ltr));
}
}
return points;
}
// Set the window shape in needed, lets our owner do some drawing (if it wants
// to), and finally draw the border.
gboolean OnRoundedWindowExpose(GtkWidget* widget,
GdkEventExpose* event) {
RoundedWindowData* data = static_cast<RoundedWindowData*>(
g_object_get_data(G_OBJECT(widget), kRoundedData));
GtkAllocation allocation;
gtk_widget_get_allocation(widget, &allocation);
if (data->expected_width != allocation.width ||
data->expected_height != allocation.height) {
data->expected_width = allocation.width;
data->expected_height = allocation.height;
// We need to update the shape of the status bubble whenever our GDK
// window changes shape.
std::vector<GdkPoint> mask_points = MakeFramePolygonPoints(
data, FRAME_MASK);
GdkRegion* mask_region = gdk_region_polygon(&mask_points[0],
mask_points.size(),
GDK_EVEN_ODD_RULE);
gdk_window_shape_combine_region(gtk_widget_get_window(widget),
mask_region, 0, 0);
gdk_region_destroy(mask_region);
}
GdkDrawable* drawable = GDK_DRAWABLE(event->window);
GdkGC* gc = gdk_gc_new(drawable);
gdk_gc_set_clip_rectangle(gc, &event->area);
gdk_gc_set_rgb_fg_color(gc, &data->border_color);
// Stroke the frame border.
std::vector<GdkPoint> points = MakeFramePolygonPoints(
data, FRAME_STROKE);
if (data->drawn_borders == BORDER_ALL) {
// If we want to have borders everywhere, we need to draw a polygon instead
// of a set of lines.
gdk_draw_polygon(drawable, gc, FALSE, &points[0], points.size());
} else if (!points.empty()) {
gdk_draw_lines(drawable, gc, &points[0], points.size());
}
g_object_unref(gc);
return FALSE; // Propagate so our children paint, etc.
}
// On theme changes, window shapes are reset, but we detect whether we need to
// reshape a window by whether its allocation has changed so force it to reset
// the window shape on next expose.
void OnStyleSet(GtkWidget* widget, GtkStyle* previous_style) {
DCHECK(widget);
RoundedWindowData* data = static_cast<RoundedWindowData*>(
g_object_get_data(G_OBJECT(widget), kRoundedData));
DCHECK(data);
data->expected_width = -1;
data->expected_height = -1;
}
} // namespace
void ActAsRoundedWindow(
GtkWidget* widget, const GdkColor& color, int corner_size,
int rounded_edges, int drawn_borders) {
DCHECK(widget);
DCHECK(!g_object_get_data(G_OBJECT(widget), kRoundedData));
gtk_widget_set_app_paintable(widget, TRUE);
RoundedWindowData* data = new RoundedWindowData;
data->signals.Connect(widget, "expose-event",
G_CALLBACK(OnRoundedWindowExpose), NULL);
data->signals.Connect(widget, "style-set", G_CALLBACK(OnStyleSet), NULL);
data->expected_width = -1;
data->expected_height = -1;
data->border_color = color;
data->corner_size = corner_size;
data->rounded_edges = rounded_edges;
data->drawn_borders = drawn_borders;
g_object_set_data_full(G_OBJECT(widget), kRoundedData,
data, FreeRoundedWindowData);
if (gtk_widget_get_visible(widget))
gtk_widget_queue_draw(widget);
}
void StopActingAsRoundedWindow(GtkWidget* widget) {
g_object_set_data(G_OBJECT(widget), kRoundedData, NULL);
if (gtk_widget_get_realized(widget))
gdk_window_shape_combine_mask(gtk_widget_get_window(widget), NULL, 0, 0);
if (gtk_widget_get_visible(widget))
gtk_widget_queue_draw(widget);
}
bool IsActingAsRoundedWindow(GtkWidget* widget) {
return g_object_get_data(G_OBJECT(widget), kRoundedData) != NULL;
}
void SetRoundedWindowEdgesAndBorders(GtkWidget* widget,
int corner_size,
int rounded_edges,
int drawn_borders) {
DCHECK(widget);
RoundedWindowData* data = static_cast<RoundedWindowData*>(
g_object_get_data(G_OBJECT(widget), kRoundedData));
DCHECK(data);
data->corner_size = corner_size;
data->rounded_edges = rounded_edges;
data->drawn_borders = drawn_borders;
}
void SetRoundedWindowBorderColor(GtkWidget* widget, GdkColor color) {
DCHECK(widget);
RoundedWindowData* data = static_cast<RoundedWindowData*>(
g_object_get_data(G_OBJECT(widget), kRoundedData));
DCHECK(data);
data->border_color = color;
}
} // namespace gtk_util