1// Copyright (c) 2011 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 "chrome/browser/ui/gtk/rounded_window.h"
6
7#include <gtk/gtk.h>
8#include <math.h>
9
10#include "base/i18n/rtl.h"
11#include "base/logging.h"
12#include "chrome/browser/ui/gtk/gtk_util.h"
13#include "ui/base/gtk/gtk_signal_registrar.h"
14
15namespace gtk_util {
16
17namespace {
18
19const char* kRoundedData = "rounded-window-data";
20
21// If the border radius is less than |kMinRoundedBorderSize|, we don't actually
22// round the corners, we just truncate the corner.
23const int kMinRoundedBorderSize = 8;
24
25struct RoundedWindowData {
26  // Expected window size. Used to detect when we need to reshape the window.
27  int expected_width;
28  int expected_height;
29
30  // Color of the border.
31  GdkColor border_color;
32
33  // Radius of the edges in pixels.
34  int corner_size;
35
36  // Which corners should be rounded?
37  int rounded_edges;
38
39  // Which sides of the window should have an internal border?
40  int drawn_borders;
41
42  // Keeps track of attached signal handlers.
43  ui::GtkSignalRegistrar signals;
44};
45
46// Callback from GTK to release allocated memory.
47void FreeRoundedWindowData(gpointer data) {
48  delete static_cast<RoundedWindowData*>(data);
49}
50
51enum FrameType {
52  FRAME_MASK,
53  FRAME_STROKE,
54};
55
56// Returns a list of points that either form the outline of the status bubble
57// (|type| == FRAME_MASK) or form the inner border around the inner edge
58// (|type| == FRAME_STROKE).
59std::vector<GdkPoint> MakeFramePolygonPoints(RoundedWindowData* data,
60                                             FrameType type) {
61  using gtk_util::MakeBidiGdkPoint;
62  int width = data->expected_width;
63  int height = data->expected_height;
64  int corner_size = data->corner_size;
65
66  std::vector<GdkPoint> points;
67
68  bool ltr = !base::i18n::IsRTL();
69  // If we have a stroke, we have to offset some of our points by 1 pixel.
70  // We have to inset by 1 pixel when we draw horizontal lines that are on the
71  // bottom or when we draw vertical lines that are closer to the end (end is
72  // right for ltr).
73  int y_off = (type == FRAME_MASK) ? 0 : -1;
74  // We use this one for LTR.
75  int x_off_l = ltr ? y_off : 0;
76  // We use this one for RTL.
77  int x_off_r = !ltr ? -y_off : 0;
78
79  // Build up points starting with the bottom left corner and continuing
80  // clockwise.
81
82  // Bottom left corner.
83  if (type == FRAME_MASK ||
84      (data->drawn_borders & (BORDER_LEFT | BORDER_BOTTOM))) {
85    if (data->rounded_edges & ROUNDED_BOTTOM_LEFT) {
86      if (corner_size >= kMinRoundedBorderSize) {
87        // We are careful to only add points that are horizontal or vertically
88        // offset from the previous point (not both).  This avoids rounding
89        // differences when two points are connected.
90        for (int x = 0; x <= corner_size; ++x) {
91          int y = static_cast<int>(sqrt(static_cast<double>(
92              (corner_size * corner_size) - (x * x))));
93          if (x > 0) {
94            points.push_back(MakeBidiGdkPoint(
95                corner_size - x + x_off_r + 1,
96                height - (corner_size - y) + y_off, width, ltr));
97          }
98          points.push_back(MakeBidiGdkPoint(
99              corner_size - x + x_off_r,
100              height - (corner_size - y) + y_off, width, ltr));
101        }
102      } else {
103        points.push_back(MakeBidiGdkPoint(
104            corner_size + x_off_l, height + y_off, width, ltr));
105        points.push_back(MakeBidiGdkPoint(
106            x_off_r, height - corner_size, width, ltr));
107      }
108    } else {
109      points.push_back(MakeBidiGdkPoint(x_off_r, height + y_off, width, ltr));
110    }
111  }
112
113  // Top left corner.
114  if (type == FRAME_MASK ||
115      (data->drawn_borders & (BORDER_LEFT | BORDER_TOP))) {
116    if (data->rounded_edges & ROUNDED_TOP_LEFT) {
117      if (corner_size >= kMinRoundedBorderSize) {
118        for (int x = corner_size; x >= 0; --x) {
119          int y = static_cast<int>(sqrt(static_cast<double>(
120              (corner_size * corner_size) - (x * x))));
121          points.push_back(MakeBidiGdkPoint(corner_size - x + x_off_r,
122              corner_size - y, width, ltr));
123          if (x > 0) {
124            points.push_back(MakeBidiGdkPoint(corner_size - x + 1 + x_off_r,
125                corner_size - y, width, ltr));
126          }
127        }
128      } else {
129        points.push_back(MakeBidiGdkPoint(
130            x_off_r, corner_size - 1, width, ltr));
131        points.push_back(MakeBidiGdkPoint(
132            corner_size + x_off_r - 1, 0, width, ltr));
133      }
134    } else {
135      points.push_back(MakeBidiGdkPoint(x_off_r, 0, width, ltr));
136    }
137  }
138
139  // Top right corner.
140  if (type == FRAME_MASK ||
141      (data->drawn_borders & (BORDER_TOP | BORDER_RIGHT))) {
142    if (data->rounded_edges & ROUNDED_TOP_RIGHT) {
143      if (corner_size >= kMinRoundedBorderSize) {
144        for (int x = 0; x <= corner_size; ++x) {
145          int y = static_cast<int>(sqrt(static_cast<double>(
146              (corner_size * corner_size) - (x * x))));
147          if (x > 0) {
148            points.push_back(MakeBidiGdkPoint(
149                width - (corner_size - x) + x_off_l - 1,
150                corner_size - y, width, ltr));
151          }
152          points.push_back(MakeBidiGdkPoint(
153              width - (corner_size - x) + x_off_l,
154              corner_size - y, width, ltr));
155        }
156      } else {
157        points.push_back(MakeBidiGdkPoint(
158            width - corner_size + 1 + x_off_l, 0, width, ltr));
159        points.push_back(MakeBidiGdkPoint(
160            width + x_off_l, corner_size - 1, width, ltr));
161      }
162    } else {
163      points.push_back(MakeBidiGdkPoint(
164          width + x_off_l, 0, width, ltr));
165    }
166  }
167
168  // Bottom right corner.
169  if (type == FRAME_MASK ||
170      (data->drawn_borders & (BORDER_RIGHT | BORDER_BOTTOM))) {
171    if (data->rounded_edges & ROUNDED_BOTTOM_RIGHT) {
172      if (corner_size >= kMinRoundedBorderSize) {
173        for (int x = corner_size; x >= 0; --x) {
174          int y = static_cast<int>(sqrt(static_cast<double>(
175              (corner_size * corner_size) - (x * x))));
176          points.push_back(MakeBidiGdkPoint(
177              width - (corner_size - x) + x_off_l,
178              height - (corner_size - y) + y_off, width, ltr));
179          if (x > 0) {
180            points.push_back(MakeBidiGdkPoint(
181                width - (corner_size - x) + x_off_l - 1,
182                height - (corner_size - y) + y_off, width, ltr));
183          }
184        }
185      } else {
186        points.push_back(MakeBidiGdkPoint(
187            width + x_off_l, height - corner_size, width, ltr));
188        points.push_back(MakeBidiGdkPoint(
189            width - corner_size + x_off_r, height + y_off, width, ltr));
190      }
191    } else {
192      points.push_back(MakeBidiGdkPoint(
193          width + x_off_l, height + y_off, width, ltr));
194    }
195  }
196
197  return points;
198}
199
200// Set the window shape in needed, lets our owner do some drawing (if it wants
201// to), and finally draw the border.
202gboolean OnRoundedWindowExpose(GtkWidget* widget,
203                               GdkEventExpose* event) {
204  RoundedWindowData* data = static_cast<RoundedWindowData*>(
205      g_object_get_data(G_OBJECT(widget), kRoundedData));
206
207  if (data->expected_width != widget->allocation.width ||
208      data->expected_height != widget->allocation.height) {
209    data->expected_width = widget->allocation.width;
210    data->expected_height = widget->allocation.height;
211
212    // We need to update the shape of the status bubble whenever our GDK
213    // window changes shape.
214    std::vector<GdkPoint> mask_points = MakeFramePolygonPoints(
215        data, FRAME_MASK);
216    GdkRegion* mask_region = gdk_region_polygon(&mask_points[0],
217                                                mask_points.size(),
218                                                GDK_EVEN_ODD_RULE);
219    gdk_window_shape_combine_region(widget->window, mask_region, 0, 0);
220    gdk_region_destroy(mask_region);
221  }
222
223  GdkDrawable* drawable = GDK_DRAWABLE(event->window);
224  GdkGC* gc = gdk_gc_new(drawable);
225  gdk_gc_set_clip_rectangle(gc, &event->area);
226  gdk_gc_set_rgb_fg_color(gc, &data->border_color);
227
228  // Stroke the frame border.
229  std::vector<GdkPoint> points = MakeFramePolygonPoints(
230      data, FRAME_STROKE);
231  if (data->drawn_borders == BORDER_ALL) {
232    // If we want to have borders everywhere, we need to draw a polygon instead
233    // of a set of lines.
234    gdk_draw_polygon(drawable, gc, FALSE, &points[0], points.size());
235  } else if (!points.empty()) {
236    gdk_draw_lines(drawable, gc, &points[0], points.size());
237  }
238
239  g_object_unref(gc);
240  return FALSE;  // Propagate so our children paint, etc.
241}
242
243// On theme changes, window shapes are reset, but we detect whether we need to
244// reshape a window by whether its allocation has changed so force it to reset
245// the window shape on next expose.
246void OnStyleSet(GtkWidget* widget, GtkStyle* previous_style) {
247  DCHECK(widget);
248  RoundedWindowData* data = static_cast<RoundedWindowData*>(
249      g_object_get_data(G_OBJECT(widget), kRoundedData));
250  DCHECK(data);
251  data->expected_width = -1;
252  data->expected_height = -1;
253}
254
255}  // namespace
256
257void ActAsRoundedWindow(
258    GtkWidget* widget, const GdkColor& color, int corner_size,
259    int rounded_edges, int drawn_borders) {
260  DCHECK(widget);
261  DCHECK(!g_object_get_data(G_OBJECT(widget), kRoundedData));
262
263  gtk_widget_set_app_paintable(widget, TRUE);
264
265  RoundedWindowData* data = new RoundedWindowData;
266  data->signals.Connect(widget, "expose-event",
267                        G_CALLBACK(OnRoundedWindowExpose), NULL);
268  data->signals.Connect(widget, "style-set", G_CALLBACK(OnStyleSet), NULL);
269
270  data->expected_width = -1;
271  data->expected_height = -1;
272
273  data->border_color = color;
274  data->corner_size = corner_size;
275
276  data->rounded_edges = rounded_edges;
277  data->drawn_borders = drawn_borders;
278
279  g_object_set_data_full(G_OBJECT(widget), kRoundedData,
280                         data, FreeRoundedWindowData);
281
282  if (GTK_WIDGET_VISIBLE(widget))
283    gtk_widget_queue_draw(widget);
284}
285
286void StopActingAsRoundedWindow(GtkWidget* widget) {
287  g_object_set_data(G_OBJECT(widget), kRoundedData, NULL);
288
289  if (GTK_WIDGET_REALIZED(widget))
290    gdk_window_shape_combine_mask(widget->window, NULL, 0, 0);
291
292  if (GTK_WIDGET_VISIBLE(widget))
293    gtk_widget_queue_draw(widget);
294}
295
296bool IsActingAsRoundedWindow(GtkWidget* widget) {
297  return g_object_get_data(G_OBJECT(widget), kRoundedData) != NULL;
298}
299
300void SetRoundedWindowEdgesAndBorders(GtkWidget* widget,
301                                     int corner_size,
302                                     int rounded_edges,
303                                     int drawn_borders) {
304  DCHECK(widget);
305  RoundedWindowData* data = static_cast<RoundedWindowData*>(
306      g_object_get_data(G_OBJECT(widget), kRoundedData));
307  DCHECK(data);
308  data->corner_size = corner_size;
309  data->rounded_edges = rounded_edges;
310  data->drawn_borders = drawn_borders;
311}
312
313void SetRoundedWindowBorderColor(GtkWidget* widget, GdkColor color) {
314  DCHECK(widget);
315  RoundedWindowData* data = static_cast<RoundedWindowData*>(
316      g_object_get_data(G_OBJECT(widget), kRoundedData));
317  DCHECK(data);
318  data->border_color = color;
319}
320
321}  // namespace gtk_util
322