1 /* GTK - The GIMP Toolkit
2  * Copyright (C) 2010 Carlos Garnacho <carlosg@gnome.org>
3  * Copyright (C) 2011 Red Hat, Inc.
4  *
5  * Authors: Carlos Garnacho <carlosg@gnome.org>
6  *          Cosimo Cecchi <cosimoc@gnome.org>
7  *
8  * This library is free software; you can redistribute it and/or
9  * modify it under the terms of the GNU Lesser General Public
10  * License as published by the Free Software Foundation; either
11  * version 2 of the License, or (at your option) any later version.
12  *
13  * This library is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16  * Lesser General Public License for more details.
17  *
18  * You should have received a copy of the GNU Lesser General Public
19  * License along with this library. If not, see <http://www.gnu.org/licenses/>.
20  */
21 
22 #include "config.h"
23 
24 #include "gtkrenderbackgroundprivate.h"
25 
26 #include "gtkcssarrayvalueprivate.h"
27 #include "gtkcssbgsizevalueprivate.h"
28 #include "gtkcsscornervalueprivate.h"
29 #include "gtkcssenumvalueprivate.h"
30 #include "gtkcssimagevalueprivate.h"
31 #include "gtkcssnumbervalueprivate.h"
32 #include "gtkcssshadowsvalueprivate.h"
33 #include "gtkcsspositionvalueprivate.h"
34 #include "gtkcssrepeatvalueprivate.h"
35 #include "gtkcssrgbavalueprivate.h"
36 #include "gtkcssstyleprivate.h"
37 #include "gtkcsstypesprivate.h"
38 
39 #include <math.h>
40 
41 #include <gdk/gdk.h>
42 
43 /* this is in case round() is not provided by the compiler,
44  * such as in the case of C89 compilers, like MSVC
45  */
46 #include "fallback-c89.c"
47 
48 typedef struct _GtkThemingBackground GtkThemingBackground;
49 
50 #define N_BOXES (3)
51 
52 struct _GtkThemingBackground {
53   GtkCssStyle *style;
54 
55   GtkRoundedBox boxes[N_BOXES];
56 };
57 
58 static void
_gtk_theming_background_paint_color(GtkThemingBackground * bg,cairo_t * cr,const GdkRGBA * bg_color,GtkCssValue * background_image)59 _gtk_theming_background_paint_color (GtkThemingBackground *bg,
60                                      cairo_t              *cr,
61                                      const GdkRGBA        *bg_color,
62                                      GtkCssValue          *background_image)
63 {
64   gint n_values = _gtk_css_array_value_get_n_values (background_image);
65   GtkCssArea clip = _gtk_css_area_value_get
66     (_gtk_css_array_value_get_nth
67      (gtk_css_style_get_value (bg->style, GTK_CSS_PROPERTY_BACKGROUND_CLIP),
68       n_values - 1));
69 
70   _gtk_rounded_box_path (&bg->boxes[clip], cr);
71   gdk_cairo_set_source_rgba (cr, bg_color);
72   cairo_fill (cr);
73 }
74 
75 static gboolean
_gtk_theming_background_needs_push_group(GtkCssStyle * style)76 _gtk_theming_background_needs_push_group (GtkCssStyle *style)
77 {
78   GtkCssValue *blend_modes;
79   gint i;
80 
81   blend_modes = gtk_css_style_get_value (style, GTK_CSS_PROPERTY_BACKGROUND_BLEND_MODE);
82 
83   /*
84    * If we have any blend mode different than NORMAL, we'll need to
85    * push a group in order to correctly apply the blend modes.
86    */
87   for (i = _gtk_css_array_value_get_n_values (blend_modes); i > 0; i--)
88     {
89       GtkCssBlendMode blend_mode;
90 
91       blend_mode = _gtk_css_blend_mode_value_get (_gtk_css_array_value_get_nth (blend_modes, i - 1));
92 
93       if (blend_mode != GTK_CSS_BLEND_MODE_NORMAL)
94         return TRUE;
95     }
96 
97   return FALSE;
98 }
99 
100 static void
_gtk_theming_background_paint_layer(GtkThemingBackground * bg,guint idx,cairo_t * cr,GtkCssBlendMode blend_mode)101 _gtk_theming_background_paint_layer (GtkThemingBackground *bg,
102                                      guint                 idx,
103                                      cairo_t              *cr,
104                                      GtkCssBlendMode       blend_mode)
105 {
106   GtkCssRepeatStyle hrepeat, vrepeat;
107   const GtkCssValue *pos, *repeat;
108   GtkCssImage *image;
109   const GtkRoundedBox *origin;
110   double image_width, image_height;
111   double width, height;
112 
113   pos = _gtk_css_array_value_get_nth (gtk_css_style_get_value (bg->style, GTK_CSS_PROPERTY_BACKGROUND_POSITION), idx);
114   repeat = _gtk_css_array_value_get_nth (gtk_css_style_get_value (bg->style, GTK_CSS_PROPERTY_BACKGROUND_REPEAT), idx);
115   hrepeat = _gtk_css_background_repeat_value_get_x (repeat);
116   vrepeat = _gtk_css_background_repeat_value_get_y (repeat);
117   image = _gtk_css_image_value_get_image (
118               _gtk_css_array_value_get_nth (
119                   gtk_css_style_get_value (bg->style, GTK_CSS_PROPERTY_BACKGROUND_IMAGE),
120                   idx));
121   origin = &bg->boxes[
122                _gtk_css_area_value_get (
123                    _gtk_css_array_value_get_nth (
124                        gtk_css_style_get_value (bg->style, GTK_CSS_PROPERTY_BACKGROUND_ORIGIN),
125                        idx))];
126   width = origin->box.width;
127   height = origin->box.height;
128 
129   if (image == NULL || width <= 0 || height <= 0)
130     return;
131 
132   _gtk_css_bg_size_value_compute_size (_gtk_css_array_value_get_nth (gtk_css_style_get_value (bg->style, GTK_CSS_PROPERTY_BACKGROUND_SIZE), idx),
133                                        image,
134                                        width,
135                                        height,
136                                        &image_width,
137                                        &image_height);
138 
139   if (image_width <= 0 || image_height <= 0)
140     return;
141 
142   /* optimization */
143   if (image_width == width)
144     hrepeat = GTK_CSS_REPEAT_STYLE_NO_REPEAT;
145   if (image_height == height)
146     vrepeat = GTK_CSS_REPEAT_STYLE_NO_REPEAT;
147 
148 
149   cairo_save (cr);
150 
151   _gtk_rounded_box_path (
152       &bg->boxes[
153           _gtk_css_area_value_get (
154               _gtk_css_array_value_get_nth (
155                   gtk_css_style_get_value (bg->style, GTK_CSS_PROPERTY_BACKGROUND_CLIP),
156                   idx))],
157       cr);
158   cairo_clip (cr);
159 
160 
161   cairo_translate (cr, origin->box.x, origin->box.y);
162 
163   /*
164    * Apply the blend mode, if any.
165    */
166   if (G_UNLIKELY (_gtk_css_blend_mode_get_operator (blend_mode) != cairo_get_operator (cr)))
167     cairo_set_operator (cr, _gtk_css_blend_mode_get_operator (blend_mode));
168 
169 
170   if (hrepeat == GTK_CSS_REPEAT_STYLE_NO_REPEAT && vrepeat == GTK_CSS_REPEAT_STYLE_NO_REPEAT)
171     {
172       cairo_translate (cr,
173                        _gtk_css_position_value_get_x (pos, width - image_width),
174                        _gtk_css_position_value_get_y (pos, height - image_height));
175       /* shortcut for normal case */
176       _gtk_css_image_draw (image, cr, image_width, image_height);
177     }
178   else
179     {
180       int surface_width, surface_height;
181       cairo_rectangle_t fill_rect;
182       cairo_surface_t *surface;
183       cairo_t *cr2;
184 
185       /* If ‘background-repeat’ is ‘round’ for one (or both) dimensions,
186        * there is a second step. The UA must scale the image in that
187        * dimension (or both dimensions) so that it fits a whole number of
188        * times in the background positioning area. In the case of the width
189        * (height is analogous):
190        *
191        * If X ≠ 0 is the width of the image after step one and W is the width
192        * of the background positioning area, then the rounded width
193        * X' = W / round(W / X) where round() is a function that returns the
194        * nearest natural number (integer greater than zero).
195        *
196        * If ‘background-repeat’ is ‘round’ for one dimension only and if
197        * ‘background-size’ is ‘auto’ for the other dimension, then there is
198        * a third step: that other dimension is scaled so that the original
199        * aspect ratio is restored.
200        */
201       if (hrepeat == GTK_CSS_REPEAT_STYLE_ROUND)
202         {
203           double n = round (width / image_width);
204 
205           n = MAX (1, n);
206 
207           if (vrepeat != GTK_CSS_REPEAT_STYLE_ROUND
208               /* && vsize == auto (it is by default) */)
209             image_height *= width / (image_width * n);
210           image_width = width / n;
211         }
212       if (vrepeat == GTK_CSS_REPEAT_STYLE_ROUND)
213         {
214           double n = round (height / image_height);
215 
216           n = MAX (1, n);
217 
218           if (hrepeat != GTK_CSS_REPEAT_STYLE_ROUND
219               /* && hsize == auto (it is by default) */)
220             image_width *= height / (image_height * n);
221           image_height = height / n;
222         }
223 
224       /* if hrepeat or vrepeat is 'space', we create a somewhat larger surface
225        * to store the extra space. */
226       if (hrepeat == GTK_CSS_REPEAT_STYLE_SPACE)
227         {
228           double n = floor (width / image_width);
229           surface_width = n ? round (width / n) : 0;
230         }
231       else
232         surface_width = round (image_width);
233 
234       if (vrepeat == GTK_CSS_REPEAT_STYLE_SPACE)
235         {
236           double n = floor (height / image_height);
237           surface_height = n ? round (height / n) : 0;
238         }
239       else
240         surface_height = round (image_height);
241 
242       surface = cairo_surface_create_similar (cairo_get_target (cr),
243                                               CAIRO_CONTENT_COLOR_ALPHA,
244                                               surface_width, surface_height);
245       cr2 = cairo_create (surface);
246       cairo_translate (cr2,
247                        0.5 * (surface_width - image_width),
248                        0.5 * (surface_height - image_height));
249       _gtk_css_image_draw (image, cr2, image_width, image_height);
250       cairo_destroy (cr2);
251 
252       cairo_set_source_surface (cr, surface,
253                                 _gtk_css_position_value_get_x (pos, width - image_width),
254                                 _gtk_css_position_value_get_y (pos, height - image_height));
255       cairo_pattern_set_extend (cairo_get_source (cr), CAIRO_EXTEND_REPEAT);
256       cairo_surface_destroy (surface);
257 
258       if (hrepeat == GTK_CSS_REPEAT_STYLE_NO_REPEAT)
259         {
260           fill_rect.x = _gtk_css_position_value_get_x (pos, width - image_width);
261           fill_rect.width = image_width;
262         }
263       else
264         {
265           fill_rect.x = 0;
266           fill_rect.width = width;
267         }
268 
269       if (vrepeat == GTK_CSS_REPEAT_STYLE_NO_REPEAT)
270         {
271           fill_rect.y = _gtk_css_position_value_get_y (pos, height - image_height);
272           fill_rect.height = image_height;
273         }
274       else
275         {
276           fill_rect.y = 0;
277           fill_rect.height = height;
278         }
279 
280       cairo_rectangle (cr, fill_rect.x, fill_rect.y,
281                        fill_rect.width, fill_rect.height);
282       cairo_fill (cr);
283     }
284 
285   /*
286    * Since this cairo_t can be shared with other widgets,
287    * we must reset the operator after all the backgrounds
288    * are properly rendered.
289    */
290   cairo_set_operator (cr, CAIRO_OPERATOR_OVER);
291 
292   cairo_restore (cr);
293 }
294 
295 static void
_gtk_theming_background_init_style(GtkThemingBackground * bg,double width,double height,GtkJunctionSides junction)296 _gtk_theming_background_init_style (GtkThemingBackground *bg,
297                                     double                width,
298                                     double                height,
299                                     GtkJunctionSides      junction)
300 {
301   GtkBorder border, padding;
302 
303   border.top = _gtk_css_number_value_get (gtk_css_style_get_value (bg->style, GTK_CSS_PROPERTY_BORDER_TOP_WIDTH), 100);
304   border.right = _gtk_css_number_value_get (gtk_css_style_get_value (bg->style, GTK_CSS_PROPERTY_BORDER_RIGHT_WIDTH), 100);
305   border.bottom = _gtk_css_number_value_get (gtk_css_style_get_value (bg->style, GTK_CSS_PROPERTY_BORDER_BOTTOM_WIDTH), 100);
306   border.left = _gtk_css_number_value_get (gtk_css_style_get_value (bg->style, GTK_CSS_PROPERTY_BORDER_LEFT_WIDTH), 100);
307   padding.top = _gtk_css_number_value_get (gtk_css_style_get_value (bg->style, GTK_CSS_PROPERTY_PADDING_TOP), 100);
308   padding.right = _gtk_css_number_value_get (gtk_css_style_get_value (bg->style, GTK_CSS_PROPERTY_PADDING_RIGHT), 100);
309   padding.bottom = _gtk_css_number_value_get (gtk_css_style_get_value (bg->style, GTK_CSS_PROPERTY_PADDING_BOTTOM), 100);
310   padding.left = _gtk_css_number_value_get (gtk_css_style_get_value (bg->style, GTK_CSS_PROPERTY_PADDING_LEFT), 100);
311 
312   /* In the CSS box model, by default the background positioning area is
313    * the padding-box, i.e. all the border-box minus the borders themselves,
314    * which determines also its default size, see
315    * http://dev.w3.org/csswg/css3-background/#background-origin
316    *
317    * In the future we might want to support different origins or clips, but
318    * right now we just shrink to the default.
319    */
320   _gtk_rounded_box_init_rect (&bg->boxes[GTK_CSS_AREA_BORDER_BOX], 0, 0, width, height);
321   _gtk_rounded_box_apply_border_radius_for_style (&bg->boxes[GTK_CSS_AREA_BORDER_BOX], bg->style, junction);
322 
323   bg->boxes[GTK_CSS_AREA_PADDING_BOX] = bg->boxes[GTK_CSS_AREA_BORDER_BOX];
324   _gtk_rounded_box_shrink (&bg->boxes[GTK_CSS_AREA_PADDING_BOX],
325 			   border.top, border.right,
326 			   border.bottom, border.left);
327 
328   bg->boxes[GTK_CSS_AREA_CONTENT_BOX] = bg->boxes[GTK_CSS_AREA_PADDING_BOX];
329   _gtk_rounded_box_shrink (&bg->boxes[GTK_CSS_AREA_CONTENT_BOX],
330 			   padding.top, padding.right,
331 			   padding.bottom, padding.left);
332 }
333 
334 void
gtk_css_style_render_background(GtkCssStyle * style,cairo_t * cr,gdouble x,gdouble y,gdouble width,gdouble height,GtkJunctionSides junction)335 gtk_css_style_render_background (GtkCssStyle      *style,
336                                  cairo_t          *cr,
337                                  gdouble           x,
338                                  gdouble           y,
339                                  gdouble           width,
340                                  gdouble           height,
341                                  GtkJunctionSides  junction)
342 {
343   GtkThemingBackground bg;
344   gint idx;
345   GtkCssValue *background_image;
346   GtkCssValue *blend_modes;
347   GtkCssValue *box_shadow;
348   const GdkRGBA *bg_color;
349   gboolean needs_push_group;
350   gint number_of_layers;
351 
352   background_image = gtk_css_style_get_value (style, GTK_CSS_PROPERTY_BACKGROUND_IMAGE);
353   blend_modes = gtk_css_style_get_value (style, GTK_CSS_PROPERTY_BACKGROUND_BLEND_MODE);
354   bg_color = _gtk_css_rgba_value_get_rgba (gtk_css_style_get_value (style, GTK_CSS_PROPERTY_BACKGROUND_COLOR));
355   box_shadow = gtk_css_style_get_value (style, GTK_CSS_PROPERTY_BOX_SHADOW);
356 
357   /* This is the common default case of no background */
358   if (gtk_rgba_is_clear (bg_color) &&
359       _gtk_css_array_value_get_n_values (background_image) == 1 &&
360       _gtk_css_image_value_get_image (_gtk_css_array_value_get_nth (background_image, 0)) == NULL &&
361       _gtk_css_shadows_value_is_none (box_shadow))
362     return;
363 
364   bg.style = style;
365   _gtk_theming_background_init_style (&bg, width, height, junction);
366 
367   cairo_save (cr);
368   cairo_translate (cr, x, y);
369 
370   /* Outset shadows */
371   _gtk_css_shadows_value_paint_box (box_shadow,
372                                     cr,
373                                     &bg.boxes[GTK_CSS_AREA_BORDER_BOX],
374                                     FALSE);
375 
376   /*
377    * When we have a blend mode set for the background, we cannot blend the current
378    * widget's drawing with whatever the content that the Cairo context may have.
379    * Because of that, push the drawing to a new group before drawing the background
380    * layers, and paint the resulting image back after.
381    */
382   needs_push_group = _gtk_theming_background_needs_push_group (style);
383 
384   if (needs_push_group)
385     {
386       cairo_save (cr);
387       cairo_rectangle (cr, 0, 0, width, height);
388       cairo_clip (cr);
389       cairo_push_group (cr);
390     }
391 
392   _gtk_theming_background_paint_color (&bg, cr, bg_color, background_image);
393 
394   number_of_layers = _gtk_css_array_value_get_n_values (background_image);
395 
396   for (idx = number_of_layers - 1; idx >= 0; idx--)
397     {
398       GtkCssBlendMode blend_mode;
399 
400       blend_mode = _gtk_css_blend_mode_value_get (_gtk_css_array_value_get_nth (blend_modes, idx));
401 
402       _gtk_theming_background_paint_layer (&bg, idx, cr, blend_mode);
403     }
404 
405   /* Paint back the resulting surface */
406   if (needs_push_group)
407     {
408       cairo_pop_group_to_source (cr);
409       cairo_paint (cr);
410       cairo_restore (cr);
411     }
412 
413   /* Inset shadows */
414   _gtk_css_shadows_value_paint_box (box_shadow,
415                                     cr,
416                                     &bg.boxes[GTK_CSS_AREA_PADDING_BOX],
417                                     TRUE);
418 
419   cairo_restore (cr);
420 }
421 
422 static gboolean
corner_value_is_right_angle(GtkCssValue * value)423 corner_value_is_right_angle (GtkCssValue *value)
424 {
425   return _gtk_css_corner_value_get_x (value, 100) <= 0.0 &&
426          _gtk_css_corner_value_get_y (value, 100) <= 0.0;
427 }
428 
429 gboolean
gtk_css_style_render_background_is_opaque(GtkCssStyle * style)430 gtk_css_style_render_background_is_opaque (GtkCssStyle *style)
431 {
432   const GdkRGBA *color;
433 
434   color = _gtk_css_rgba_value_get_rgba (gtk_css_style_get_value (style, GTK_CSS_PROPERTY_BACKGROUND_COLOR));
435 
436   return color->alpha >= 1.0
437       && corner_value_is_right_angle (gtk_css_style_get_value (style, GTK_CSS_PROPERTY_BORDER_TOP_LEFT_RADIUS))
438       && corner_value_is_right_angle (gtk_css_style_get_value (style, GTK_CSS_PROPERTY_BORDER_TOP_RIGHT_RADIUS))
439       && corner_value_is_right_angle (gtk_css_style_get_value (style, GTK_CSS_PROPERTY_BORDER_BOTTOM_RIGHT_RADIUS))
440       && corner_value_is_right_angle (gtk_css_style_get_value (style, GTK_CSS_PROPERTY_BORDER_BOTTOM_LEFT_RADIUS));
441 }
442