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
15 namespace gtk_util {
16
17 namespace {
18
19 const 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.
23 const int kMinRoundedBorderSize = 8;
24
25 struct 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.
FreeRoundedWindowData(gpointer data)47 void FreeRoundedWindowData(gpointer data) {
48 delete static_cast<RoundedWindowData*>(data);
49 }
50
51 enum 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).
MakeFramePolygonPoints(RoundedWindowData * data,FrameType type)59 std::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.
OnRoundedWindowExpose(GtkWidget * widget,GdkEventExpose * event)202 gboolean 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.
OnStyleSet(GtkWidget * widget,GtkStyle * previous_style)246 void 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
ActAsRoundedWindow(GtkWidget * widget,const GdkColor & color,int corner_size,int rounded_edges,int drawn_borders)257 void 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
StopActingAsRoundedWindow(GtkWidget * widget)286 void 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
IsActingAsRoundedWindow(GtkWidget * widget)296 bool IsActingAsRoundedWindow(GtkWidget* widget) {
297 return g_object_get_data(G_OBJECT(widget), kRoundedData) != NULL;
298 }
299
SetRoundedWindowEdgesAndBorders(GtkWidget * widget,int corner_size,int rounded_edges,int drawn_borders)300 void 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
SetRoundedWindowBorderColor(GtkWidget * widget,GdkColor color)313 void 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