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