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