1 /*
2  * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
3  *
4  * SPDX-License-Identifier: LGPL-2.1+
5  */
6 
7 #include "config.h"
8 #include <glib/gi18n-lib.h>
9 
10 #include "hdy-cairo-private.h"
11 #include "hdy-shadow-helper-private.h"
12 
13 #include <math.h>
14 
15 /**
16  * PRIVATE:hdy-shadow-helper
17  * @short_description: Shadow helper used in #HdyLeaflet
18  * @title: HdyShadowHelper
19  * @See_also: #HdyLeaflet
20  * @stability: Private
21  *
22  * A helper class for drawing #HdyLeaflet transition shadow.
23  *
24  * Since: 0.0.12
25  */
26 
27 struct _HdyShadowHelper
28 {
29   GObject parent_instance;
30 
31   GtkWidget *widget;
32 
33   gboolean is_cache_valid;
34 
35   cairo_pattern_t *dimming_pattern;
36   cairo_pattern_t *shadow_pattern;
37   cairo_pattern_t *border_pattern;
38   cairo_pattern_t *outline_pattern;
39   gint shadow_size;
40   gint border_size;
41   gint outline_size;
42 
43   GtkPanDirection last_direction;
44   gint last_width;
45   gint last_height;
46   gint last_scale;
47 };
48 
49 G_DEFINE_TYPE (HdyShadowHelper, hdy_shadow_helper, G_TYPE_OBJECT);
50 
51 enum {
52   PROP_0,
53   PROP_WIDGET,
54   LAST_PROP,
55 };
56 
57 static GParamSpec *props[LAST_PROP];
58 
59 
60 static GtkStyleContext *
create_context(HdyShadowHelper * self,const gchar * name,GtkPanDirection direction)61 create_context (HdyShadowHelper *self,
62                 const gchar     *name,
63                 GtkPanDirection  direction)
64 {
65   g_autoptr(GtkWidgetPath) path = NULL;
66   GtkStyleContext *context;
67   gint pos;
68   const gchar *direction_name;
69   GEnumClass *enum_class;
70 
71   enum_class = g_type_class_ref (GTK_TYPE_PAN_DIRECTION);
72   direction_name = g_enum_get_value (enum_class, direction)->value_nick;
73 
74   path = gtk_widget_path_copy (gtk_widget_get_path (self->widget));
75 
76   pos = gtk_widget_path_append_type (path, GTK_TYPE_WIDGET);
77   gtk_widget_path_iter_set_object_name (path, pos, name);
78 
79   gtk_widget_path_iter_add_class (path, pos, direction_name);
80 
81   context = gtk_style_context_new ();
82   gtk_style_context_set_path (context, path);
83 
84   g_type_class_unref (enum_class);
85 
86   return context;
87 }
88 
89 static gint
get_element_size(GtkStyleContext * context,GtkPanDirection direction)90 get_element_size (GtkStyleContext *context,
91                   GtkPanDirection  direction)
92 {
93   gint width, height;
94 
95   gtk_style_context_get (context,
96                          gtk_style_context_get_state (context),
97                          "min-width", &width,
98                          "min-height", &height,
99                          NULL);
100 
101   switch (direction) {
102   case GTK_PAN_DIRECTION_LEFT:
103   case GTK_PAN_DIRECTION_RIGHT:
104     return width;
105   case GTK_PAN_DIRECTION_UP:
106   case GTK_PAN_DIRECTION_DOWN:
107     return height;
108   default:
109     g_assert_not_reached ();
110   }
111 
112   return 0;
113 }
114 
115 static cairo_pattern_t *
create_element_pattern(GtkStyleContext * context,gint width,gint height,gint scale)116 create_element_pattern (GtkStyleContext *context,
117                         gint             width,
118                         gint             height,
119                         gint             scale)
120 {
121   g_autoptr (cairo_surface_t) surface =
122     cairo_image_surface_create (CAIRO_FORMAT_ARGB32, width * scale, height * scale);
123   g_autoptr (cairo_t) cr = cairo_create (surface);
124   cairo_pattern_t *pattern;
125 
126   cairo_surface_set_device_scale (surface, scale, scale);
127 
128   gtk_render_background (context, cr, 0, 0, width, height);
129   gtk_render_frame (context, cr, 0, 0, width, height);
130 
131   pattern = cairo_pattern_create_for_surface (surface);
132 
133   return pattern;
134 }
135 
136 static void
cache_shadow(HdyShadowHelper * self,gint width,gint height,GtkPanDirection direction)137 cache_shadow (HdyShadowHelper *self,
138               gint             width,
139               gint             height,
140               GtkPanDirection  direction)
141 {
142   g_autoptr(GtkStyleContext) dim_context = NULL;
143   g_autoptr(GtkStyleContext) shadow_context = NULL;
144   g_autoptr(GtkStyleContext) border_context = NULL;
145   g_autoptr(GtkStyleContext) outline_context = NULL;
146   gint shadow_size, border_size, outline_size, scale;
147 
148   scale = gtk_widget_get_scale_factor (self->widget);
149 
150   if (self->last_direction == direction &&
151       self->last_width == width &&
152       self->last_height == height &&
153       self->last_scale == scale &&
154       self->is_cache_valid)
155     return;
156 
157   hdy_shadow_helper_clear_cache (self);
158 
159   dim_context = create_context (self, "dimming", direction);
160   shadow_context = create_context (self, "shadow", direction);
161   border_context = create_context (self, "border", direction);
162   outline_context = create_context (self, "outline", direction);
163 
164   shadow_size = get_element_size (shadow_context, direction);
165   border_size = get_element_size (border_context, direction);
166   outline_size = get_element_size (outline_context, direction);
167 
168   self->dimming_pattern = create_element_pattern (dim_context, width, height, scale);
169   if (direction == GTK_PAN_DIRECTION_LEFT || direction == GTK_PAN_DIRECTION_RIGHT) {
170     self->shadow_pattern = create_element_pattern (shadow_context, shadow_size, height, scale);
171     self->border_pattern = create_element_pattern (border_context, border_size, height, scale);
172     self->outline_pattern = create_element_pattern (outline_context, outline_size, height, scale);
173   } else {
174     self->shadow_pattern = create_element_pattern (shadow_context, width, shadow_size, scale);
175     self->border_pattern = create_element_pattern (border_context, width, border_size, scale);
176     self->outline_pattern = create_element_pattern (outline_context, width, outline_size, scale);
177   }
178 
179   self->border_size = border_size;
180   self->shadow_size = shadow_size;
181   self->outline_size = outline_size;
182 
183   self->is_cache_valid = TRUE;
184   self->last_direction = direction;
185   self->last_width = width;
186   self->last_height = height;
187   self->last_scale = scale;
188 }
189 
190 static void
hdy_shadow_helper_dispose(GObject * object)191 hdy_shadow_helper_dispose (GObject *object)
192 {
193   HdyShadowHelper *self = HDY_SHADOW_HELPER (object);
194 
195   hdy_shadow_helper_clear_cache (self);
196 
197   if (self->widget)
198     g_clear_object (&self->widget);
199 
200   G_OBJECT_CLASS (hdy_shadow_helper_parent_class)->dispose (object);
201 }
202 
203 static void
hdy_shadow_helper_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)204 hdy_shadow_helper_get_property (GObject    *object,
205                                 guint       prop_id,
206                                 GValue     *value,
207                                 GParamSpec *pspec)
208 {
209   HdyShadowHelper *self = HDY_SHADOW_HELPER (object);
210 
211   switch (prop_id) {
212   case PROP_WIDGET:
213     g_value_set_object (value, self->widget);
214     break;
215 
216   default:
217     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
218   }
219 }
220 
221 static void
hdy_shadow_helper_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)222 hdy_shadow_helper_set_property (GObject      *object,
223                                 guint         prop_id,
224                                 const GValue *value,
225                                 GParamSpec   *pspec)
226 {
227   HdyShadowHelper *self = HDY_SHADOW_HELPER (object);
228 
229   switch (prop_id) {
230   case PROP_WIDGET:
231     self->widget = GTK_WIDGET (g_object_ref (g_value_get_object (value)));
232     break;
233 
234   default:
235     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
236   }
237 }
238 
239 static void
hdy_shadow_helper_class_init(HdyShadowHelperClass * klass)240 hdy_shadow_helper_class_init (HdyShadowHelperClass *klass)
241 {
242   GObjectClass *object_class = G_OBJECT_CLASS (klass);
243 
244   object_class->dispose = hdy_shadow_helper_dispose;
245   object_class->get_property = hdy_shadow_helper_get_property;
246   object_class->set_property = hdy_shadow_helper_set_property;
247 
248   /**
249    * HdyShadowHelper:widget:
250    *
251    * The widget the shadow will be drawn for. Must not be %NULL
252    *
253    * Since: 0.0.11
254    */
255   props[PROP_WIDGET] =
256     g_param_spec_object ("widget",
257                          _("Widget"),
258                          _("The widget the shadow will be drawn for"),
259                          GTK_TYPE_WIDGET,
260                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
261 
262   g_object_class_install_properties (object_class, LAST_PROP, props);
263 }
264 
265 static void
hdy_shadow_helper_init(HdyShadowHelper * self)266 hdy_shadow_helper_init (HdyShadowHelper *self)
267 {
268 }
269 
270 /**
271  * hdy_shadow_helper_new:
272  *
273  * Creates a new #HdyShadowHelper object.
274  *
275  * Returns: The newly created #HdyShadowHelper object
276  *
277  * Since: 0.0.12
278  */
279 HdyShadowHelper *
hdy_shadow_helper_new(GtkWidget * widget)280 hdy_shadow_helper_new (GtkWidget *widget)
281 {
282   return g_object_new (HDY_TYPE_SHADOW_HELPER,
283                        "widget", widget,
284                        NULL);
285 }
286 
287 /**
288  * hdy_shadow_helper_clear_cache:
289  * @self: a #HdyShadowHelper
290  *
291  * Clears shadow cache. This should be used after a transition is done.
292  *
293  * Since: 0.0.12
294  */
295 void
hdy_shadow_helper_clear_cache(HdyShadowHelper * self)296 hdy_shadow_helper_clear_cache (HdyShadowHelper *self)
297 {
298   if (!self->is_cache_valid)
299     return;
300 
301   cairo_pattern_destroy (self->dimming_pattern);
302   cairo_pattern_destroy (self->shadow_pattern);
303   cairo_pattern_destroy (self->border_pattern);
304   cairo_pattern_destroy (self->outline_pattern);
305   self->border_size = 0;
306   self->shadow_size = 0;
307   self->outline_size = 0;
308 
309   self->last_direction = 0;
310   self->last_width = 0;
311   self->last_height = 0;
312   self->last_scale = 0;
313 
314   self->is_cache_valid = FALSE;
315 }
316 
317 /**
318  * hdy_shadow_helper_draw_shadow:
319  * @self: a #HdyShadowHelper
320  * @cr: a Cairo context to draw to
321  * @width: the width of the shadow rectangle
322  * @height: the height of the shadow rectangle
323  * @progress: transition progress, changes from 0 to 1
324  * @direction: shadow direction
325  *
326  * Draws a transition shadow. For caching to work, @width, @height and
327  * @direction shouldn't change between calls.
328  *
329  * Since: 0.0.12
330  */
331 void
hdy_shadow_helper_draw_shadow(HdyShadowHelper * self,cairo_t * cr,gint width,gint height,gdouble progress,GtkPanDirection direction)332 hdy_shadow_helper_draw_shadow (HdyShadowHelper *self,
333                                cairo_t         *cr,
334                                gint             width,
335                                gint             height,
336                                gdouble          progress,
337                                GtkPanDirection  direction)
338 {
339   gdouble remaining_distance, shadow_opacity;
340   gint shadow_size, border_size, outline_size, distance;
341 
342   if (progress >= 1)
343     return;
344 
345   cache_shadow (self, width, height, direction);
346 
347   shadow_size = self->shadow_size;
348   border_size = self->border_size;
349   outline_size = self->outline_size;
350 
351   switch (direction) {
352   case GTK_PAN_DIRECTION_LEFT:
353   case GTK_PAN_DIRECTION_RIGHT:
354     distance = width;
355     break;
356   case GTK_PAN_DIRECTION_UP:
357   case GTK_PAN_DIRECTION_DOWN:
358     distance = height;
359     break;
360   default:
361     g_assert_not_reached ();
362   }
363 
364   remaining_distance = (1 - progress) * (gdouble) distance;
365   shadow_opacity = 1;
366   if (remaining_distance < shadow_size)
367     shadow_opacity = (remaining_distance / shadow_size);
368 
369   cairo_save (cr);
370 
371   switch (direction) {
372   case GTK_PAN_DIRECTION_LEFT:
373     cairo_rectangle (cr, -outline_size, 0, width + outline_size, height);
374     break;
375   case GTK_PAN_DIRECTION_RIGHT:
376     cairo_rectangle (cr, 0, 0, width + outline_size, height);
377     break;
378   case GTK_PAN_DIRECTION_UP:
379     cairo_rectangle (cr, 0, -outline_size, width, height + outline_size);
380     break;
381   case GTK_PAN_DIRECTION_DOWN:
382     cairo_rectangle (cr, 0, 0, width, height + outline_size);
383     break;
384   default:
385     g_assert_not_reached ();
386   }
387   cairo_clip (cr);
388   gdk_window_mark_paint_from_clip (gtk_widget_get_window (self->widget), cr);
389 
390   cairo_set_source (cr, self->dimming_pattern);
391   cairo_paint_with_alpha (cr, 1 - progress);
392 
393   switch (direction) {
394   case GTK_PAN_DIRECTION_RIGHT:
395     cairo_translate (cr, width - shadow_size, 0);
396     break;
397   case GTK_PAN_DIRECTION_DOWN:
398     cairo_translate (cr, 0, height - shadow_size);
399     break;
400   case GTK_PAN_DIRECTION_LEFT:
401   case GTK_PAN_DIRECTION_UP:
402     break;
403   default:
404     g_assert_not_reached ();
405   }
406 
407   cairo_set_source (cr, self->shadow_pattern);
408   cairo_paint_with_alpha (cr, shadow_opacity);
409 
410   switch (direction) {
411   case GTK_PAN_DIRECTION_RIGHT:
412     cairo_translate (cr, shadow_size - border_size, 0);
413     break;
414   case GTK_PAN_DIRECTION_DOWN:
415     cairo_translate (cr, 0, shadow_size - border_size);
416     break;
417   case GTK_PAN_DIRECTION_LEFT:
418   case GTK_PAN_DIRECTION_UP:
419     break;
420   default:
421     g_assert_not_reached ();
422   }
423 
424   cairo_set_source (cr, self->border_pattern);
425   cairo_paint (cr);
426 
427   switch (direction) {
428   case GTK_PAN_DIRECTION_RIGHT:
429     cairo_translate (cr, border_size, 0);
430     break;
431   case GTK_PAN_DIRECTION_DOWN:
432     cairo_translate (cr, 0, border_size);
433     break;
434   case GTK_PAN_DIRECTION_LEFT:
435     cairo_translate (cr, -outline_size, 0);
436     break;
437   case GTK_PAN_DIRECTION_UP:
438     cairo_translate (cr, 0, -outline_size);
439     break;
440   default:
441     g_assert_not_reached ();
442   }
443 
444   cairo_set_source (cr, self->outline_pattern);
445   cairo_paint (cr);
446 
447   cairo_restore (cr);
448 }
449