1 /*
2  * Copyright © 2012 Red Hat Inc.
3  *
4  * This library is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Lesser General Public
6  * License as published by the Free Software Foundation; either
7  * version 2.1 of the License, or (at your option) any later version.
8  *
9  * This library is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * Lesser General Public License for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public
15  * License along with this library. If not, see <http://www.gnu.org/licenses/>.
16  *
17  * Authors: Benjamin Otte <otte@gnome.org>
18  */
19 
20 #include "config.h"
21 
22 #include "gtkcssimagelinearprivate.h"
23 
24 #include <math.h>
25 
26 #include "gtkcsscolorvalueprivate.h"
27 #include "gtkcssnumbervalueprivate.h"
28 #include "gtkcssrgbavalueprivate.h"
29 #include "gtkcssprovider.h"
30 
G_DEFINE_TYPE(GtkCssImageLinear,_gtk_css_image_linear,GTK_TYPE_CSS_IMAGE)31 G_DEFINE_TYPE (GtkCssImageLinear, _gtk_css_image_linear, GTK_TYPE_CSS_IMAGE)
32 
33 static void
34 gtk_css_image_linear_get_start_end (GtkCssImageLinear *linear,
35                                     double             length,
36                                     double            *start,
37                                     double            *end)
38 {
39   GtkCssImageLinearColorStop *stop;
40   double pos;
41   guint i;
42 
43   if (linear->repeating)
44     {
45       stop = &g_array_index (linear->stops, GtkCssImageLinearColorStop, 0);
46       if (stop->offset == NULL)
47         *start = 0;
48       else
49         *start = _gtk_css_number_value_get (stop->offset, length) / length;
50 
51       *end = *start;
52 
53       for (i = 0; i < linear->stops->len; i++)
54         {
55           stop = &g_array_index (linear->stops, GtkCssImageLinearColorStop, i);
56 
57           if (stop->offset == NULL)
58             continue;
59 
60           pos = _gtk_css_number_value_get (stop->offset, length) / length;
61 
62           *end = MAX (pos, *end);
63         }
64 
65       if (stop->offset == NULL)
66         *end = MAX (*end, 1.0);
67     }
68   else
69     {
70       *start = 0;
71       *end = 1;
72     }
73 }
74 
75 static void
gtk_css_image_linear_compute_start_point(double angle_in_degrees,double width,double height,double * x,double * y)76 gtk_css_image_linear_compute_start_point (double angle_in_degrees,
77                                           double width,
78                                           double height,
79                                           double *x,
80                                           double *y)
81 {
82   double angle, c, slope, perpendicular;
83 
84   angle = fmod (angle_in_degrees, 360);
85   if (angle < 0)
86     angle += 360;
87 
88   if (angle == 0)
89     {
90       *x = 0;
91       *y = -height;
92       return;
93     }
94   else if (angle == 90)
95     {
96       *x = width;
97       *y = 0;
98       return;
99     }
100   else if (angle == 180)
101     {
102       *x = 0;
103       *y = height;
104       return;
105     }
106   else if (angle == 270)
107     {
108       *x = -width;
109       *y = 0;
110       return;
111     }
112 
113   /* The tan() is confusing because the angle is clockwise
114    * from 'to top' */
115   perpendicular = tan (angle * G_PI / 180);
116   slope = -1 / perpendicular;
117 
118   if (angle > 180)
119     width = - width;
120   if (angle < 90 || angle > 270)
121     height = - height;
122 
123   /* Compute c (of y = mx + c) of perpendicular */
124   c = height - perpendicular * width;
125 
126   *x = c / (slope - perpendicular);
127   *y = perpendicular * *x + c;
128 }
129 
130 static void
gtk_css_image_linear_draw(GtkCssImage * image,cairo_t * cr,double width,double height)131 gtk_css_image_linear_draw (GtkCssImage        *image,
132                            cairo_t            *cr,
133                            double              width,
134                            double              height)
135 {
136   GtkCssImageLinear *linear = GTK_CSS_IMAGE_LINEAR (image);
137   cairo_pattern_t *pattern;
138   double angle; /* actual angle of the gradiant line in degrees */
139   double x, y; /* coordinates of start point */
140   double length; /* distance in pixels for 100% */
141   double start, end; /* position of first/last point on gradient line - with gradient line being [0, 1] */
142   double offset;
143   int i, last;
144 
145   if (linear->side)
146     {
147       /* special casing the regular cases here so we don't get rounding errors */
148       switch (linear->side)
149       {
150         case 1 << GTK_CSS_RIGHT:
151           angle = 90;
152           break;
153         case 1 << GTK_CSS_LEFT:
154           angle = 270;
155           break;
156         case 1 << GTK_CSS_TOP:
157           angle = 0;
158           break;
159         case 1 << GTK_CSS_BOTTOM:
160           angle = 180;
161           break;
162         default:
163           angle = atan2 (linear->side & 1 << GTK_CSS_TOP ? -width : width,
164                          linear->side & 1 << GTK_CSS_LEFT ? -height : height);
165           angle = 180 * angle / G_PI + 90;
166           break;
167       }
168     }
169   else
170     {
171       angle = _gtk_css_number_value_get (linear->angle, 100);
172     }
173 
174   gtk_css_image_linear_compute_start_point (angle,
175                                             width, height,
176                                             &x, &y);
177 
178   length = sqrt (x * x + y * y);
179   gtk_css_image_linear_get_start_end (linear, length, &start, &end);
180   pattern = cairo_pattern_create_linear (x * (start - 0.5), y * (start - 0.5),
181                                          x * (end - 0.5),   y * (end - 0.5));
182   if (linear->repeating)
183     cairo_pattern_set_extend (pattern, CAIRO_EXTEND_REPEAT);
184   else
185     cairo_pattern_set_extend (pattern, CAIRO_EXTEND_PAD);
186 
187   offset = start;
188   last = -1;
189   for (i = 0; i < linear->stops->len; i++)
190     {
191       GtkCssImageLinearColorStop *stop;
192       double pos, step;
193 
194       stop = &g_array_index (linear->stops, GtkCssImageLinearColorStop, i);
195 
196       if (stop->offset == NULL)
197         {
198           if (i == 0)
199             pos = 0.0;
200           else if (i + 1 == linear->stops->len)
201             pos = 1.0;
202           else
203             continue;
204         }
205       else
206         pos = _gtk_css_number_value_get (stop->offset, length) / length;
207 
208       pos = MAX (pos, offset);
209       step = (pos - offset) / (i - last);
210       for (last = last + 1; last <= i; last++)
211         {
212           const GdkRGBA *rgba;
213 
214           stop = &g_array_index (linear->stops, GtkCssImageLinearColorStop, last);
215 
216           rgba = _gtk_css_rgba_value_get_rgba (stop->color);
217           offset += step;
218 
219           cairo_pattern_add_color_stop_rgba (pattern,
220                                              (offset - start) / (end - start),
221                                              rgba->red,
222                                              rgba->green,
223                                              rgba->blue,
224                                              rgba->alpha);
225         }
226 
227       offset = pos;
228       last = i;
229     }
230 
231   cairo_rectangle (cr, 0, 0, width, height);
232   cairo_translate (cr, width / 2, height / 2);
233   cairo_set_source (cr, pattern);
234   cairo_fill (cr);
235 
236   cairo_pattern_destroy (pattern);
237 }
238 
239 
240 static gboolean
gtk_css_image_linear_parse(GtkCssImage * image,GtkCssParser * parser)241 gtk_css_image_linear_parse (GtkCssImage  *image,
242                             GtkCssParser *parser)
243 {
244   GtkCssImageLinear *linear = GTK_CSS_IMAGE_LINEAR (image);
245   guint i;
246 
247   if (_gtk_css_parser_try (parser, "repeating-linear-gradient(", TRUE))
248     linear->repeating = TRUE;
249   else if (_gtk_css_parser_try (parser, "linear-gradient(", TRUE))
250     linear->repeating = FALSE;
251   else
252     {
253       _gtk_css_parser_error (parser, "Not a linear gradient");
254       return FALSE;
255     }
256 
257   if (_gtk_css_parser_try (parser, "to", TRUE))
258     {
259       for (i = 0; i < 2; i++)
260         {
261           if (_gtk_css_parser_try (parser, "left", TRUE))
262             {
263               if (linear->side & ((1 << GTK_CSS_LEFT) | (1 << GTK_CSS_RIGHT)))
264                 {
265                   _gtk_css_parser_error (parser, "Expected 'top', 'bottom' or comma");
266                   return FALSE;
267                 }
268               linear->side |= (1 << GTK_CSS_LEFT);
269             }
270           else if (_gtk_css_parser_try (parser, "right", TRUE))
271             {
272               if (linear->side & ((1 << GTK_CSS_LEFT) | (1 << GTK_CSS_RIGHT)))
273                 {
274                   _gtk_css_parser_error (parser, "Expected 'top', 'bottom' or comma");
275                   return FALSE;
276                 }
277               linear->side |= (1 << GTK_CSS_RIGHT);
278             }
279           else if (_gtk_css_parser_try (parser, "top", TRUE))
280             {
281               if (linear->side & ((1 << GTK_CSS_TOP) | (1 << GTK_CSS_BOTTOM)))
282                 {
283                   _gtk_css_parser_error (parser, "Expected 'left', 'right' or comma");
284                   return FALSE;
285                 }
286               linear->side |= (1 << GTK_CSS_TOP);
287             }
288           else if (_gtk_css_parser_try (parser, "bottom", TRUE))
289             {
290               if (linear->side & ((1 << GTK_CSS_TOP) | (1 << GTK_CSS_BOTTOM)))
291                 {
292                   _gtk_css_parser_error (parser, "Expected 'left', 'right' or comma");
293                   return FALSE;
294                 }
295               linear->side |= (1 << GTK_CSS_BOTTOM);
296             }
297           else
298             break;
299         }
300 
301       if (linear->side == 0)
302         {
303           _gtk_css_parser_error (parser, "Expected side that gradient should go to");
304           return FALSE;
305         }
306 
307       if (!_gtk_css_parser_try (parser, ",", TRUE))
308         {
309           _gtk_css_parser_error (parser, "Expected a comma");
310           return FALSE;
311         }
312     }
313   else if (gtk_css_number_value_can_parse (parser))
314     {
315       linear->angle = _gtk_css_number_value_parse (parser, GTK_CSS_PARSE_ANGLE);
316       if (linear->angle == NULL)
317         return FALSE;
318 
319       if (!_gtk_css_parser_try (parser, ",", TRUE))
320         {
321           _gtk_css_parser_error (parser, "Expected a comma");
322           return FALSE;
323         }
324     }
325   else
326     linear->side = 1 << GTK_CSS_BOTTOM;
327 
328   do {
329     GtkCssImageLinearColorStop stop;
330 
331     stop.color = _gtk_css_color_value_parse (parser);
332     if (stop.color == NULL)
333       return FALSE;
334 
335     if (gtk_css_number_value_can_parse (parser))
336       {
337         stop.offset = _gtk_css_number_value_parse (parser,
338                                                    GTK_CSS_PARSE_PERCENT
339                                                    | GTK_CSS_PARSE_LENGTH);
340         if (stop.offset == NULL)
341           {
342             _gtk_css_value_unref (stop.color);
343             return FALSE;
344           }
345       }
346     else
347       {
348         stop.offset = NULL;
349       }
350 
351     g_array_append_val (linear->stops, stop);
352 
353   } while (_gtk_css_parser_try (parser, ",", TRUE));
354 
355   if (linear->stops->len < 2)
356     {
357       _gtk_css_parser_error_full (parser,
358                                   GTK_CSS_PROVIDER_ERROR_DEPRECATED,
359                                   "Using one color stop with %s() is deprecated.",
360                                   linear->repeating ? "repeating-linear-gradient" : "linear-gradient");
361     }
362 
363   if (!_gtk_css_parser_try (parser, ")", TRUE))
364     {
365       _gtk_css_parser_error (parser, "Missing closing bracket at end of linear gradient");
366       return FALSE;
367     }
368 
369   return TRUE;
370 }
371 
372 static void
gtk_css_image_linear_print(GtkCssImage * image,GString * string)373 gtk_css_image_linear_print (GtkCssImage *image,
374                             GString     *string)
375 {
376   GtkCssImageLinear *linear = GTK_CSS_IMAGE_LINEAR (image);
377   guint i;
378 
379   if (linear->repeating)
380     g_string_append (string, "repeating-linear-gradient(");
381   else
382     g_string_append (string, "linear-gradient(");
383 
384   if (linear->side)
385     {
386       if (linear->side != (1 << GTK_CSS_BOTTOM))
387         {
388           g_string_append (string, "to");
389 
390           if (linear->side & (1 << GTK_CSS_TOP))
391             g_string_append (string, " top");
392           else if (linear->side & (1 << GTK_CSS_BOTTOM))
393             g_string_append (string, " bottom");
394 
395           if (linear->side & (1 << GTK_CSS_LEFT))
396             g_string_append (string, " left");
397           else if (linear->side & (1 << GTK_CSS_RIGHT))
398             g_string_append (string, " right");
399 
400           g_string_append (string, ", ");
401         }
402     }
403   else
404     {
405       _gtk_css_value_print (linear->angle, string);
406       g_string_append (string, ", ");
407     }
408 
409   for (i = 0; i < linear->stops->len; i++)
410     {
411       GtkCssImageLinearColorStop *stop;
412 
413       if (i > 0)
414         g_string_append (string, ", ");
415 
416       stop = &g_array_index (linear->stops, GtkCssImageLinearColorStop, i);
417 
418       _gtk_css_value_print (stop->color, string);
419 
420       if (stop->offset)
421         {
422           g_string_append (string, " ");
423           _gtk_css_value_print (stop->offset, string);
424         }
425     }
426 
427   g_string_append (string, ")");
428 }
429 
430 static GtkCssImage *
gtk_css_image_linear_compute(GtkCssImage * image,guint property_id,GtkStyleProviderPrivate * provider,GtkCssStyle * style,GtkCssStyle * parent_style)431 gtk_css_image_linear_compute (GtkCssImage             *image,
432                               guint                    property_id,
433                               GtkStyleProviderPrivate *provider,
434                               GtkCssStyle             *style,
435                               GtkCssStyle             *parent_style)
436 {
437   GtkCssImageLinear *linear = GTK_CSS_IMAGE_LINEAR (image);
438   GtkCssImageLinear *copy;
439   guint i;
440 
441   copy = g_object_new (GTK_TYPE_CSS_IMAGE_LINEAR, NULL);
442   copy->repeating = linear->repeating;
443   copy->side = linear->side;
444 
445   if (linear->angle)
446     copy->angle = _gtk_css_value_compute (linear->angle, property_id, provider, style, parent_style);
447 
448   g_array_set_size (copy->stops, linear->stops->len);
449   for (i = 0; i < linear->stops->len; i++)
450     {
451       GtkCssImageLinearColorStop *stop, *scopy;
452 
453       stop = &g_array_index (linear->stops, GtkCssImageLinearColorStop, i);
454       scopy = &g_array_index (copy->stops, GtkCssImageLinearColorStop, i);
455 
456       scopy->color = _gtk_css_value_compute (stop->color, property_id, provider, style, parent_style);
457 
458       if (stop->offset)
459         {
460           scopy->offset = _gtk_css_value_compute (stop->offset, property_id, provider, style, parent_style);
461         }
462       else
463         {
464           scopy->offset = NULL;
465         }
466     }
467 
468   return GTK_CSS_IMAGE (copy);
469 }
470 
471 static GtkCssImage *
gtk_css_image_linear_transition(GtkCssImage * start_image,GtkCssImage * end_image,guint property_id,double progress)472 gtk_css_image_linear_transition (GtkCssImage *start_image,
473                                  GtkCssImage *end_image,
474                                  guint        property_id,
475                                  double       progress)
476 {
477   GtkCssImageLinear *start, *end, *result;
478   guint i;
479 
480   start = GTK_CSS_IMAGE_LINEAR (start_image);
481 
482   if (end_image == NULL)
483     return GTK_CSS_IMAGE_CLASS (_gtk_css_image_linear_parent_class)->transition (start_image, end_image, property_id, progress);
484 
485   if (!GTK_IS_CSS_IMAGE_LINEAR (end_image))
486     return GTK_CSS_IMAGE_CLASS (_gtk_css_image_linear_parent_class)->transition (start_image, end_image, property_id, progress);
487 
488   end = GTK_CSS_IMAGE_LINEAR (end_image);
489 
490   if ((start->repeating != end->repeating)
491       || (start->stops->len != end->stops->len))
492     return GTK_CSS_IMAGE_CLASS (_gtk_css_image_linear_parent_class)->transition (start_image, end_image, property_id, progress);
493 
494   result = g_object_new (GTK_TYPE_CSS_IMAGE_LINEAR, NULL);
495   result->repeating = start->repeating;
496 
497   if (start->side != end->side)
498     goto fail;
499 
500   result->side = start->side;
501   if (result->side == 0)
502     result->angle = _gtk_css_value_transition (start->angle, end->angle, property_id, progress);
503   if (result->angle == NULL)
504     goto fail;
505 
506   for (i = 0; i < start->stops->len; i++)
507     {
508       GtkCssImageLinearColorStop stop, *start_stop, *end_stop;
509 
510       start_stop = &g_array_index (start->stops, GtkCssImageLinearColorStop, i);
511       end_stop = &g_array_index (end->stops, GtkCssImageLinearColorStop, i);
512 
513       if ((start_stop->offset != NULL) != (end_stop->offset != NULL))
514         goto fail;
515 
516       if (start_stop->offset == NULL)
517         {
518           stop.offset = NULL;
519         }
520       else
521         {
522           stop.offset = _gtk_css_value_transition (start_stop->offset,
523                                                    end_stop->offset,
524                                                    property_id,
525                                                    progress);
526           if (stop.offset == NULL)
527             goto fail;
528         }
529 
530       stop.color = _gtk_css_value_transition (start_stop->color,
531                                               end_stop->color,
532                                               property_id,
533                                               progress);
534       if (stop.color == NULL)
535         {
536           if (stop.offset)
537             _gtk_css_value_unref (stop.offset);
538           goto fail;
539         }
540 
541       g_array_append_val (result->stops, stop);
542     }
543 
544   return GTK_CSS_IMAGE (result);
545 
546 fail:
547   g_object_unref (result);
548   return GTK_CSS_IMAGE_CLASS (_gtk_css_image_linear_parent_class)->transition (start_image, end_image, property_id, progress);
549 }
550 
551 static gboolean
gtk_css_image_linear_equal(GtkCssImage * image1,GtkCssImage * image2)552 gtk_css_image_linear_equal (GtkCssImage *image1,
553                             GtkCssImage *image2)
554 {
555   GtkCssImageLinear *linear1 = GTK_CSS_IMAGE_LINEAR (image1);
556   GtkCssImageLinear *linear2 = GTK_CSS_IMAGE_LINEAR (image2);
557   guint i;
558 
559   if (linear1->repeating != linear2->repeating ||
560       linear1->side != linear2->side ||
561       (linear1->side == 0 && !_gtk_css_value_equal (linear1->angle, linear2->angle)) ||
562       linear1->stops->len != linear2->stops->len)
563     return FALSE;
564 
565   for (i = 0; i < linear1->stops->len; i++)
566     {
567       GtkCssImageLinearColorStop *stop1, *stop2;
568 
569       stop1 = &g_array_index (linear1->stops, GtkCssImageLinearColorStop, i);
570       stop2 = &g_array_index (linear2->stops, GtkCssImageLinearColorStop, i);
571 
572       if (!_gtk_css_value_equal0 (stop1->offset, stop2->offset) ||
573           !_gtk_css_value_equal (stop1->color, stop2->color))
574         return FALSE;
575     }
576 
577   return TRUE;
578 }
579 
580 static void
gtk_css_image_linear_dispose(GObject * object)581 gtk_css_image_linear_dispose (GObject *object)
582 {
583   GtkCssImageLinear *linear = GTK_CSS_IMAGE_LINEAR (object);
584 
585   if (linear->stops)
586     {
587       g_array_free (linear->stops, TRUE);
588       linear->stops = NULL;
589     }
590 
591   linear->side = 0;
592   if (linear->angle)
593     {
594       _gtk_css_value_unref (linear->angle);
595       linear->angle = NULL;
596     }
597 
598   G_OBJECT_CLASS (_gtk_css_image_linear_parent_class)->dispose (object);
599 }
600 
601 static void
_gtk_css_image_linear_class_init(GtkCssImageLinearClass * klass)602 _gtk_css_image_linear_class_init (GtkCssImageLinearClass *klass)
603 {
604   GtkCssImageClass *image_class = GTK_CSS_IMAGE_CLASS (klass);
605   GObjectClass *object_class = G_OBJECT_CLASS (klass);
606 
607   image_class->draw = gtk_css_image_linear_draw;
608   image_class->parse = gtk_css_image_linear_parse;
609   image_class->print = gtk_css_image_linear_print;
610   image_class->compute = gtk_css_image_linear_compute;
611   image_class->equal = gtk_css_image_linear_equal;
612   image_class->transition = gtk_css_image_linear_transition;
613 
614   object_class->dispose = gtk_css_image_linear_dispose;
615 }
616 
617 static void
gtk_css_image_clear_color_stop(gpointer color_stop)618 gtk_css_image_clear_color_stop (gpointer color_stop)
619 {
620   GtkCssImageLinearColorStop *stop = color_stop;
621 
622   _gtk_css_value_unref (stop->color);
623   if (stop->offset)
624     _gtk_css_value_unref (stop->offset);
625 }
626 
627 static void
_gtk_css_image_linear_init(GtkCssImageLinear * linear)628 _gtk_css_image_linear_init (GtkCssImageLinear *linear)
629 {
630   linear->stops = g_array_new (FALSE, FALSE, sizeof (GtkCssImageLinearColorStop));
631   g_array_set_clear_func (linear->stops, gtk_css_image_clear_color_stop);
632 }
633 
634