// 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 #include #include "base/i18n/rtl.h" #include "base/logging.h" #include "chrome/browser/ui/gtk/gtk_util.h" #include "ui/base/gtk/gtk_compat.h" #include "ui/base/gtk/gtk_signal_registrar.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(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 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 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(sqrt(static_cast( (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(sqrt(static_cast( (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(sqrt(static_cast( (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(sqrt(static_cast( (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( 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 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 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( 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( 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( g_object_get_data(G_OBJECT(widget), kRoundedData)); DCHECK(data); data->border_color = color; } } // namespace gtk_util