1 /*
2 * Copyright © 2015 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: Matthias Clasen <mclasen@redhat.com>
18 */
19
20 #include "config.h"
21
22 #include "gtkcssimageradialprivate.h"
23
24 #include <math.h>
25
26 #include "gtkcsscolorvalueprivate.h"
27 #include "gtkcssnumbervalueprivate.h"
28 #include "gtkcsspositionvalueprivate.h"
29 #include "gtkcssrgbavalueprivate.h"
30 #include "gtkcssprovider.h"
31
G_DEFINE_TYPE(GtkCssImageRadial,_gtk_css_image_radial,GTK_TYPE_CSS_IMAGE)32 G_DEFINE_TYPE (GtkCssImageRadial, _gtk_css_image_radial, GTK_TYPE_CSS_IMAGE)
33
34 static void
35 gtk_css_image_radial_get_start_end (GtkCssImageRadial *radial,
36 double radius,
37 double *start,
38 double *end)
39 {
40 GtkCssImageRadialColorStop *stop;
41 double pos;
42 guint i;
43
44 if (radial->repeating)
45 {
46 stop = &g_array_index (radial->stops, GtkCssImageRadialColorStop, 0);
47 if (stop->offset == NULL)
48 *start = 0;
49 else
50 *start = _gtk_css_number_value_get (stop->offset, radius) / radius;
51
52 *end = *start;
53
54 for (i = 0; i < radial->stops->len; i++)
55 {
56 stop = &g_array_index (radial->stops, GtkCssImageRadialColorStop, i);
57
58 if (stop->offset == NULL)
59 continue;
60
61 pos = _gtk_css_number_value_get (stop->offset, radius) / radius;
62
63 *end = MAX (pos, *end);
64 }
65
66 if (stop->offset == NULL)
67 *end = MAX (*end, 1.0);
68 }
69 else
70 {
71 *start = 0;
72 *end = 1;
73 }
74 }
75
76 static void
gtk_css_image_radial_draw(GtkCssImage * image,cairo_t * cr,double width,double height)77 gtk_css_image_radial_draw (GtkCssImage *image,
78 cairo_t *cr,
79 double width,
80 double height)
81 {
82 GtkCssImageRadial *radial = GTK_CSS_IMAGE_RADIAL (image);
83 cairo_pattern_t *pattern;
84 cairo_matrix_t matrix;
85 double x, y;
86 double radius, yscale;
87 double start, end;
88 double r1, r2, r3, r4, r;
89 double offset;
90 int i, last;
91
92 x = _gtk_css_position_value_get_x (radial->position, width);
93 y = _gtk_css_position_value_get_y (radial->position, height);
94
95 if (radial->circle)
96 {
97 switch (radial->size)
98 {
99 case GTK_CSS_EXPLICIT_SIZE:
100 radius = _gtk_css_number_value_get (radial->sizes[0], width);
101 break;
102 case GTK_CSS_CLOSEST_SIDE:
103 radius = MIN (MIN (x, width - x), MIN (y, height - y));
104 break;
105 case GTK_CSS_FARTHEST_SIDE:
106 radius = MAX (MAX (x, width - x), MAX (y, height - y));
107 break;
108 case GTK_CSS_CLOSEST_CORNER:
109 case GTK_CSS_FARTHEST_CORNER:
110 r1 = x*x + y*y;
111 r2 = x*x + (height - y)*(height - y);
112 r3 = (width - x)*(width - x) + y*y;
113 r4 = (width - x)*(width - x) + (height - y)*(height - y);
114 if (radial->size == GTK_CSS_CLOSEST_CORNER)
115 r = MIN ( MIN (r1, r2), MIN (r3, r4));
116 else
117 r = MAX ( MAX (r1, r2), MAX (r3, r4));
118 radius = sqrt (r);
119 break;
120 default:
121 g_assert_not_reached ();
122 }
123
124 radius = MAX (1.0, radius);
125 yscale = 1.0;
126 }
127 else
128 {
129 double hradius, vradius;
130
131 switch (radial->size)
132 {
133 case GTK_CSS_EXPLICIT_SIZE:
134 hradius = _gtk_css_number_value_get (radial->sizes[0], width);
135 vradius = _gtk_css_number_value_get (radial->sizes[1], height);
136 break;
137 case GTK_CSS_CLOSEST_SIDE:
138 hradius = MIN (x, width - x);
139 vradius = MIN (y, height - y);
140 break;
141 case GTK_CSS_FARTHEST_SIDE:
142 hradius = MAX (x, width - x);
143 vradius = MAX (y, height - y);
144 break;
145 case GTK_CSS_CLOSEST_CORNER:
146 hradius = M_SQRT2 * MIN (x, width - x);
147 vradius = M_SQRT2 * MIN (y, height - y);
148 break;
149 case GTK_CSS_FARTHEST_CORNER:
150 hradius = M_SQRT2 * MAX (x, width - x);
151 vradius = M_SQRT2 * MAX (y, height - y);
152 break;
153 default:
154 g_assert_not_reached ();
155 }
156
157 hradius = MAX (1.0, hradius);
158 vradius = MAX (1.0, vradius);
159
160 radius = hradius;
161 yscale = vradius / hradius;
162 }
163
164 gtk_css_image_radial_get_start_end (radial, radius, &start, &end);
165
166 pattern = cairo_pattern_create_radial (0, 0, 0, 0, 0, radius);
167 if (yscale != 1.0)
168 {
169 cairo_matrix_init_scale (&matrix, 1.0, 1.0 / yscale);
170 cairo_pattern_set_matrix (pattern, &matrix);
171 }
172
173 if (radial->repeating)
174 cairo_pattern_set_extend (pattern, CAIRO_EXTEND_REPEAT);
175 else
176 cairo_pattern_set_extend (pattern, CAIRO_EXTEND_PAD);
177
178 offset = start;
179 last = -1;
180 for (i = 0; i < radial->stops->len; i++)
181 {
182 GtkCssImageRadialColorStop *stop;
183 double pos, step;
184
185 stop = &g_array_index (radial->stops, GtkCssImageRadialColorStop, i);
186
187 if (stop->offset == NULL)
188 {
189 if (i == 0)
190 pos = 0.0;
191 else if (i + 1 == radial->stops->len)
192 pos = 1.0;
193 else
194 continue;
195 }
196 else
197 pos = _gtk_css_number_value_get (stop->offset, radius) / radius;
198
199 pos = MAX (pos, 0);
200 step = pos / (i - last);
201 for (last = last + 1; last <= i; last++)
202 {
203 const GdkRGBA *rgba;
204
205 stop = &g_array_index (radial->stops, GtkCssImageRadialColorStop, last);
206
207 rgba = _gtk_css_rgba_value_get_rgba (stop->color);
208 offset += step;
209
210 cairo_pattern_add_color_stop_rgba (pattern,
211 (offset - start) / (end - start),
212 rgba->red,
213 rgba->green,
214 rgba->blue,
215 rgba->alpha);
216 }
217
218 offset = pos;
219 last = i;
220 }
221
222 cairo_rectangle (cr, 0, 0, width, height);
223 cairo_translate (cr, x, y);
224 cairo_set_source (cr, pattern);
225 cairo_fill (cr);
226
227 cairo_pattern_destroy (pattern);
228 }
229
230 static gboolean
gtk_css_image_radial_parse(GtkCssImage * image,GtkCssParser * parser)231 gtk_css_image_radial_parse (GtkCssImage *image,
232 GtkCssParser *parser)
233 {
234 GtkCssImageRadial *radial = GTK_CSS_IMAGE_RADIAL (image);
235 gboolean has_shape = FALSE;
236 gboolean has_size = FALSE;
237 gboolean found_one = FALSE;
238 guint i;
239 static struct {
240 const char *name;
241 guint value;
242 } names[] = {
243 { "closest-side", GTK_CSS_CLOSEST_SIDE },
244 { "farthest-side", GTK_CSS_FARTHEST_SIDE },
245 { "closest-corner", GTK_CSS_CLOSEST_CORNER },
246 { "farthest-corner", GTK_CSS_FARTHEST_CORNER }
247 };
248
249 if (_gtk_css_parser_try (parser, "repeating-radial-gradient(", TRUE))
250 radial->repeating = TRUE;
251 else if (_gtk_css_parser_try (parser, "radial-gradient(", TRUE))
252 radial->repeating = FALSE;
253 else
254 {
255 _gtk_css_parser_error (parser, "Not a radial gradient");
256 return FALSE;
257 }
258
259 do {
260 found_one = FALSE;
261 if (!has_shape && _gtk_css_parser_try (parser, "circle", TRUE))
262 {
263 radial->circle = TRUE;
264 found_one = has_shape = TRUE;
265 }
266 else if (!has_shape && _gtk_css_parser_try (parser, "ellipse", TRUE))
267 {
268 radial->circle = FALSE;
269 found_one = has_shape = TRUE;
270 }
271 else if (!has_size)
272 {
273 for (i = 0; i < G_N_ELEMENTS (names); i++)
274 {
275 if (_gtk_css_parser_try (parser, names[i].name, TRUE))
276 {
277 found_one = has_size = TRUE;
278 radial->size = names[i].value;
279 break;
280 }
281 }
282
283 if (!has_size)
284 {
285 if (gtk_css_number_value_can_parse (parser))
286 radial->sizes[0] = _gtk_css_number_value_parse (parser, GTK_CSS_PARSE_LENGTH | GTK_CSS_PARSE_PERCENT);
287 if (gtk_css_number_value_can_parse (parser))
288 radial->sizes[1] = _gtk_css_number_value_parse (parser, GTK_CSS_PARSE_LENGTH | GTK_CSS_PARSE_PERCENT);
289 found_one = has_size = radial->sizes[0] != NULL;
290 }
291 }
292
293 } while (found_one && !(has_shape && has_size));
294
295 if (_gtk_css_parser_try (parser, "at", TRUE))
296 {
297 radial->position = _gtk_css_position_value_parse (parser);
298 if (!radial->position)
299 return FALSE;
300 if (!_gtk_css_parser_try (parser, ",", TRUE))
301 {
302 _gtk_css_parser_error (parser, "Expected a comma here");
303 return FALSE;
304 }
305 }
306 else
307 {
308 radial->position = _gtk_css_position_value_new (_gtk_css_number_value_new (50, GTK_CSS_PERCENT),
309 _gtk_css_number_value_new (50, GTK_CSS_PERCENT));
310
311 if ((has_shape || has_size) &&
312 !_gtk_css_parser_try (parser, ",", TRUE))
313 {
314 _gtk_css_parser_error (parser, "Expected a comma here");
315 return FALSE;
316 }
317 }
318
319 if (!has_size)
320 {
321 radial->size = GTK_CSS_FARTHEST_CORNER;
322 }
323
324 if (!has_shape)
325 {
326 if (radial->sizes[0] && radial->sizes[1])
327 radial->circle = FALSE;
328 else
329 radial->circle = TRUE;
330 }
331
332 if (has_shape && radial->circle)
333 {
334 if (radial->sizes[0] && radial->sizes[1])
335 {
336 _gtk_css_parser_error (parser, "Circular gradient can only have one size");
337 return FALSE;
338 }
339
340 if (radial->sizes[0] && gtk_css_number_value_has_percent (radial->sizes[0]))
341 {
342 _gtk_css_parser_error (parser, "Circular gradient cannot have percentage as size");
343 return FALSE;
344 }
345 }
346
347 if (has_size && !radial->circle)
348 {
349 if (radial->sizes[0] && !radial->sizes[1])
350 radial->sizes[1] = _gtk_css_value_ref (radial->sizes[0]);
351 }
352
353 do {
354 GtkCssImageRadialColorStop stop;
355
356 stop.color = _gtk_css_color_value_parse (parser);
357 if (stop.color == NULL)
358 return FALSE;
359
360 if (gtk_css_number_value_can_parse (parser))
361 {
362 stop.offset = _gtk_css_number_value_parse (parser,
363 GTK_CSS_PARSE_PERCENT
364 | GTK_CSS_PARSE_LENGTH);
365 if (stop.offset == NULL)
366 {
367 _gtk_css_value_unref (stop.color);
368 return FALSE;
369 }
370 }
371 else
372 {
373 stop.offset = NULL;
374 }
375
376 g_array_append_val (radial->stops, stop);
377
378 } while (_gtk_css_parser_try (parser, ",", TRUE));
379
380 if (radial->stops->len < 2)
381 {
382 _gtk_css_parser_error_full (parser,
383 GTK_CSS_PROVIDER_ERROR_DEPRECATED,
384 "Using one color stop with %s() is deprecated.",
385 radial->repeating ? "repeating-radial-gradient" : "radial-gradient");
386 }
387
388 if (!_gtk_css_parser_try (parser, ")", TRUE))
389 {
390 _gtk_css_parser_error (parser, "Missing closing bracket at end of radial gradient");
391 return FALSE;
392 }
393
394 return TRUE;
395 }
396
397 static void
gtk_css_image_radial_print(GtkCssImage * image,GString * string)398 gtk_css_image_radial_print (GtkCssImage *image,
399 GString *string)
400 {
401 GtkCssImageRadial *radial = GTK_CSS_IMAGE_RADIAL (image);
402 guint i;
403 const gchar *names[] = {
404 NULL,
405 "closest-side",
406 "farthest-side",
407 "closest-corner",
408 "farthest-corner"
409 };
410
411 if (radial->repeating)
412 g_string_append (string, "repeating-radial-gradient(");
413 else
414 g_string_append (string, "radial-gradient(");
415
416 if (radial->circle)
417 g_string_append (string, "circle ");
418 else
419 g_string_append (string, "ellipse ");
420
421 if (radial->size != 0)
422 g_string_append (string, names[radial->size]);
423 else
424 {
425 if (radial->sizes[0])
426 _gtk_css_value_print (radial->sizes[0], string);
427 if (radial->sizes[1])
428 {
429 g_string_append (string, " ");
430 _gtk_css_value_print (radial->sizes[1], string);
431 }
432 }
433
434 g_string_append (string, " at ");
435 _gtk_css_value_print (radial->position, string);
436
437 g_string_append (string, ", ");
438
439 for (i = 0; i < radial->stops->len; i++)
440 {
441 GtkCssImageRadialColorStop *stop;
442
443 if (i > 0)
444 g_string_append (string, ", ");
445
446 stop = &g_array_index (radial->stops, GtkCssImageRadialColorStop, i);
447
448 _gtk_css_value_print (stop->color, string);
449
450 if (stop->offset)
451 {
452 g_string_append (string, " ");
453 _gtk_css_value_print (stop->offset, string);
454 }
455 }
456
457 g_string_append (string, ")");
458 }
459
460 static GtkCssImage *
gtk_css_image_radial_compute(GtkCssImage * image,guint property_id,GtkStyleProviderPrivate * provider,GtkCssStyle * style,GtkCssStyle * parent_style)461 gtk_css_image_radial_compute (GtkCssImage *image,
462 guint property_id,
463 GtkStyleProviderPrivate *provider,
464 GtkCssStyle *style,
465 GtkCssStyle *parent_style)
466 {
467 GtkCssImageRadial *radial = GTK_CSS_IMAGE_RADIAL (image);
468 GtkCssImageRadial *copy;
469 guint i;
470
471 copy = g_object_new (GTK_TYPE_CSS_IMAGE_RADIAL, NULL);
472 copy->repeating = radial->repeating;
473 copy->circle = radial->circle;
474 copy->size = radial->size;
475
476 copy->position = _gtk_css_value_compute (radial->position, property_id, provider, style, parent_style);
477
478 if (radial->sizes[0])
479 copy->sizes[0] = _gtk_css_value_compute (radial->sizes[0], property_id, provider, style, parent_style);
480
481 if (radial->sizes[1])
482 copy->sizes[1] = _gtk_css_value_compute (radial->sizes[1], property_id, provider, style, parent_style);
483
484 g_array_set_size (copy->stops, radial->stops->len);
485 for (i = 0; i < radial->stops->len; i++)
486 {
487 GtkCssImageRadialColorStop *stop, *scopy;
488
489 stop = &g_array_index (radial->stops, GtkCssImageRadialColorStop, i);
490 scopy = &g_array_index (copy->stops, GtkCssImageRadialColorStop, i);
491
492 scopy->color = _gtk_css_value_compute (stop->color, property_id, provider, style, parent_style);
493
494 if (stop->offset)
495 {
496 scopy->offset = _gtk_css_value_compute (stop->offset, property_id, provider, style, parent_style);
497 }
498 else
499 {
500 scopy->offset = NULL;
501 }
502 }
503
504 return GTK_CSS_IMAGE (copy);
505 }
506
507 static GtkCssImage *
gtk_css_image_radial_transition(GtkCssImage * start_image,GtkCssImage * end_image,guint property_id,double progress)508 gtk_css_image_radial_transition (GtkCssImage *start_image,
509 GtkCssImage *end_image,
510 guint property_id,
511 double progress)
512 {
513 GtkCssImageRadial *start, *end, *result;
514 guint i;
515
516 start = GTK_CSS_IMAGE_RADIAL (start_image);
517
518 if (end_image == NULL)
519 return GTK_CSS_IMAGE_CLASS (_gtk_css_image_radial_parent_class)->transition (start_image, end_image, property_id, progress);
520
521 if (!GTK_IS_CSS_IMAGE_RADIAL (end_image))
522 return GTK_CSS_IMAGE_CLASS (_gtk_css_image_radial_parent_class)->transition (start_image, end_image, property_id, progress);
523
524 end = GTK_CSS_IMAGE_RADIAL (end_image);
525
526 if (start->repeating != end->repeating ||
527 start->stops->len != end->stops->len ||
528 start->size != end->size ||
529 start->circle != end->circle)
530 return GTK_CSS_IMAGE_CLASS (_gtk_css_image_radial_parent_class)->transition (start_image, end_image, property_id, progress);
531
532 result = g_object_new (GTK_TYPE_CSS_IMAGE_RADIAL, NULL);
533 result->repeating = start->repeating;
534 result->circle = start->circle;
535 result->size = start->size;
536
537 result->position = _gtk_css_value_transition (start->position, end->position, property_id, progress);
538 if (result->position == NULL)
539 goto fail;
540
541 if (start->sizes[0] && end->sizes[0])
542 {
543 result->sizes[0] = _gtk_css_value_transition (start->sizes[0], end->sizes[0], property_id, progress);
544 if (result->sizes[0] == NULL)
545 goto fail;
546 }
547 else
548 result->sizes[0] = 0;
549
550 if (start->sizes[1] && end->sizes[1])
551 {
552 result->sizes[1] = _gtk_css_value_transition (start->sizes[1], end->sizes[1], property_id, progress);
553 if (result->sizes[1] == NULL)
554 goto fail;
555 }
556 else
557 result->sizes[1] = 0;
558
559 for (i = 0; i < start->stops->len; i++)
560 {
561 GtkCssImageRadialColorStop stop, *start_stop, *end_stop;
562
563 start_stop = &g_array_index (start->stops, GtkCssImageRadialColorStop, i);
564 end_stop = &g_array_index (end->stops, GtkCssImageRadialColorStop, i);
565
566 if ((start_stop->offset != NULL) != (end_stop->offset != NULL))
567 goto fail;
568
569 if (start_stop->offset == NULL)
570 {
571 stop.offset = NULL;
572 }
573 else
574 {
575 stop.offset = _gtk_css_value_transition (start_stop->offset,
576 end_stop->offset,
577 property_id,
578 progress);
579 if (stop.offset == NULL)
580 goto fail;
581 }
582
583 stop.color = _gtk_css_value_transition (start_stop->color,
584 end_stop->color,
585 property_id,
586 progress);
587 if (stop.color == NULL)
588 {
589 if (stop.offset)
590 _gtk_css_value_unref (stop.offset);
591 goto fail;
592 }
593
594 g_array_append_val (result->stops, stop);
595 }
596
597 return GTK_CSS_IMAGE (result);
598
599 fail:
600 g_object_unref (result);
601 return GTK_CSS_IMAGE_CLASS (_gtk_css_image_radial_parent_class)->transition (start_image, end_image, property_id, progress);
602 }
603
604 static gboolean
gtk_css_image_radial_equal(GtkCssImage * image1,GtkCssImage * image2)605 gtk_css_image_radial_equal (GtkCssImage *image1,
606 GtkCssImage *image2)
607 {
608 GtkCssImageRadial *radial1 = GTK_CSS_IMAGE_RADIAL (image1);
609 GtkCssImageRadial *radial2 = GTK_CSS_IMAGE_RADIAL (image2);
610 guint i;
611
612 if (radial1->repeating != radial2->repeating ||
613 radial1->size != radial2->size ||
614 !_gtk_css_value_equal (radial1->position, radial2->position) ||
615 ((radial1->sizes[0] == NULL) != (radial2->sizes[0] == NULL)) ||
616 (radial1->sizes[0] && radial2->sizes[0] && !_gtk_css_value_equal (radial1->sizes[0], radial2->sizes[0])) ||
617 ((radial1->sizes[1] == NULL) != (radial2->sizes[1] == NULL)) ||
618 (radial1->sizes[1] && radial2->sizes[1] && !_gtk_css_value_equal (radial1->sizes[1], radial2->sizes[1])) ||
619 radial1->stops->len != radial2->stops->len)
620 return FALSE;
621
622 for (i = 0; i < radial1->stops->len; i++)
623 {
624 GtkCssImageRadialColorStop *stop1, *stop2;
625
626 stop1 = &g_array_index (radial1->stops, GtkCssImageRadialColorStop, i);
627 stop2 = &g_array_index (radial2->stops, GtkCssImageRadialColorStop, i);
628
629 if (!_gtk_css_value_equal0 (stop1->offset, stop2->offset) ||
630 !_gtk_css_value_equal (stop1->color, stop2->color))
631 return FALSE;
632 }
633
634 return TRUE;
635 }
636
637 static void
gtk_css_image_radial_dispose(GObject * object)638 gtk_css_image_radial_dispose (GObject *object)
639 {
640 GtkCssImageRadial *radial = GTK_CSS_IMAGE_RADIAL (object);
641 int i;
642
643 if (radial->stops)
644 {
645 g_array_free (radial->stops, TRUE);
646 radial->stops = NULL;
647 }
648
649 if (radial->position)
650 {
651 _gtk_css_value_unref (radial->position);
652 radial->position = NULL;
653 }
654
655 for (i = 0; i < 2; i++)
656 if (radial->sizes[i])
657 {
658 _gtk_css_value_unref (radial->sizes[i]);
659 radial->sizes[i] = NULL;
660 }
661
662 G_OBJECT_CLASS (_gtk_css_image_radial_parent_class)->dispose (object);
663 }
664
665 static void
_gtk_css_image_radial_class_init(GtkCssImageRadialClass * klass)666 _gtk_css_image_radial_class_init (GtkCssImageRadialClass *klass)
667 {
668 GtkCssImageClass *image_class = GTK_CSS_IMAGE_CLASS (klass);
669 GObjectClass *object_class = G_OBJECT_CLASS (klass);
670
671 image_class->draw = gtk_css_image_radial_draw;
672 image_class->parse = gtk_css_image_radial_parse;
673 image_class->print = gtk_css_image_radial_print;
674 image_class->compute = gtk_css_image_radial_compute;
675 image_class->transition = gtk_css_image_radial_transition;
676 image_class->equal = gtk_css_image_radial_equal;
677
678 object_class->dispose = gtk_css_image_radial_dispose;
679 }
680
681 static void
gtk_css_image_clear_color_stop(gpointer color_stop)682 gtk_css_image_clear_color_stop (gpointer color_stop)
683 {
684 GtkCssImageRadialColorStop *stop = color_stop;
685
686 _gtk_css_value_unref (stop->color);
687 if (stop->offset)
688 _gtk_css_value_unref (stop->offset);
689 }
690
691 static void
_gtk_css_image_radial_init(GtkCssImageRadial * radial)692 _gtk_css_image_radial_init (GtkCssImageRadial *radial)
693 {
694 radial->stops = g_array_new (FALSE, FALSE, sizeof (GtkCssImageRadialColorStop));
695 g_array_set_clear_func (radial->stops, gtk_css_image_clear_color_stop);
696 }
697
698