1 /* gnome-bg-crossfade.h - fade window background between two surfaces
2  *
3  * Copyright (C) 2008 Red Hat, Inc.
4  *
5  * This program is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU Library General Public License
7  * as published by the Free Software Foundation; either version 2 of
8  * the License, or (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful, but
11  * WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * Library General Public License for more details.
14  *
15  * You should have received a copy of the GNU Library General Public
16  * License along with this program; if not, write to the Free Software
17  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
18  * 02110-1301, USA.
19  *
20  * Author: Ray Strode <rstrode@redhat.com>
21 */
22 
23 #include <string.h>
24 #include <math.h>
25 #include <stdarg.h>
26 
27 #include <gio/gio.h>
28 
29 #include <gdk/gdk.h>
30 #include <gdk/gdkx.h>
31 #include <X11/Xlib.h>
32 #include <X11/Xatom.h>
33 #include <gtk/gtk.h>
34 
35 #include <cairo.h>
36 #include <cairo-xlib.h>
37 
38 #define GNOME_DESKTOP_USE_UNSTABLE_API
39 #include "gnome-bg.h"
40 #include "gnome-bg-crossfade.h"
41 
42 struct _GnomeBGCrossfadePrivate
43 {
44 	GdkWindow       *window;
45 	int              width;
46 	int              height;
47 	cairo_surface_t *fading_surface;
48 	cairo_surface_t *end_surface;
49 	gdouble          start_time;
50 	gdouble          total_duration;
51 	guint            timeout_id;
52 	guint            is_first_frame : 1;
53 };
54 
55 enum {
56 	PROP_0,
57 	PROP_WIDTH,
58 	PROP_HEIGHT,
59 };
60 
61 enum {
62 	FINISHED,
63 	NUMBER_OF_SIGNALS
64 };
65 
66 static guint signals[NUMBER_OF_SIGNALS] = { 0 };
67 
G_DEFINE_TYPE(GnomeBGCrossfade,gnome_bg_crossfade,G_TYPE_OBJECT)68 G_DEFINE_TYPE (GnomeBGCrossfade, gnome_bg_crossfade, G_TYPE_OBJECT)
69 #define GNOME_BG_CROSSFADE_GET_PRIVATE(o) (G_TYPE_INSTANCE_GET_PRIVATE ((o),\
70 			                   GNOME_TYPE_BG_CROSSFADE,\
71 			                   GnomeBGCrossfadePrivate))
72 
73 static void
74 gnome_bg_crossfade_set_property (GObject      *object,
75 				 guint         property_id,
76 				 const GValue *value,
77 				 GParamSpec   *pspec)
78 {
79 	GnomeBGCrossfade *fade;
80 
81 	g_assert (GNOME_IS_BG_CROSSFADE (object));
82 
83 	fade = GNOME_BG_CROSSFADE (object);
84 
85 	switch (property_id)
86 	{
87 	case PROP_WIDTH:
88 		fade->priv->width = g_value_get_int (value);
89 		break;
90 	case PROP_HEIGHT:
91 		fade->priv->height = g_value_get_int (value);
92 		break;
93 	default:
94 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
95 		break;
96 	}
97 }
98 
99 static void
gnome_bg_crossfade_get_property(GObject * object,guint property_id,GValue * value,GParamSpec * pspec)100 gnome_bg_crossfade_get_property (GObject    *object,
101 			     guint       property_id,
102 			     GValue     *value,
103 			     GParamSpec *pspec)
104 {
105 	GnomeBGCrossfade *fade;
106 
107 	g_assert (GNOME_IS_BG_CROSSFADE (object));
108 
109 	fade = GNOME_BG_CROSSFADE (object);
110 
111 	switch (property_id)
112 	{
113 	case PROP_WIDTH:
114 		g_value_set_int (value, fade->priv->width);
115 		break;
116 	case PROP_HEIGHT:
117 		g_value_set_int (value, fade->priv->height);
118 		break;
119 
120 	default:
121 		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
122 		break;
123 	}
124 }
125 
126 static void
gnome_bg_crossfade_finalize(GObject * object)127 gnome_bg_crossfade_finalize (GObject *object)
128 {
129 	GnomeBGCrossfade *fade;
130 
131 	fade = GNOME_BG_CROSSFADE (object);
132 
133 	gnome_bg_crossfade_stop (fade);
134 
135 	if (fade->priv->fading_surface != NULL) {
136 		cairo_surface_destroy (fade->priv->fading_surface);
137 		fade->priv->fading_surface = NULL;
138 	}
139 
140 	if (fade->priv->end_surface != NULL) {
141 		g_object_unref (fade->priv->end_surface);
142 		fade->priv->end_surface = NULL;
143 	}
144 }
145 
146 static void
gnome_bg_crossfade_class_init(GnomeBGCrossfadeClass * fade_class)147 gnome_bg_crossfade_class_init (GnomeBGCrossfadeClass *fade_class)
148 {
149 	GObjectClass *gobject_class;
150 
151 	gobject_class = G_OBJECT_CLASS (fade_class);
152 
153 	gobject_class->get_property = gnome_bg_crossfade_get_property;
154 	gobject_class->set_property = gnome_bg_crossfade_set_property;
155 	gobject_class->finalize = gnome_bg_crossfade_finalize;
156 
157 	/**
158 	 * GnomeBGCrossfade:width:
159 	 *
160 	 * When a crossfade is running, this is width of the fading
161 	 * surface.
162 	 */
163 	g_object_class_install_property (gobject_class,
164 					 PROP_WIDTH,
165 					 g_param_spec_int ("width",
166 						           "Window Width",
167 							    "Width of window to fade",
168 							    0, G_MAXINT, 0,
169 							    G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE));
170 
171 	/**
172 	 * GnomeBGCrossfade:height:
173 	 *
174 	 * When a crossfade is running, this is height of the fading
175 	 * surface.
176 	 */
177 	g_object_class_install_property (gobject_class,
178 					 PROP_HEIGHT,
179 					 g_param_spec_int ("height", "Window Height",
180 						           "Height of window to fade on",
181 							   0, G_MAXINT, 0,
182 							   G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE));
183 
184 	/**
185 	 * GnomeBGCrossfade::finished:
186 	 * @fade: the #GnomeBGCrossfade that received the signal
187 	 * @window: the #GdkWindow the crossfade happend on.
188 	 *
189 	 * When a crossfade finishes, @window will have a copy
190 	 * of the end surface as its background, and this signal will
191 	 * get emitted.
192 	 */
193 	signals[FINISHED] = g_signal_new ("finished",
194 					  G_OBJECT_CLASS_TYPE (gobject_class),
195 					  G_SIGNAL_RUN_LAST, 0, NULL, NULL,
196 					  g_cclosure_marshal_VOID__OBJECT,
197 					  G_TYPE_NONE, 1, G_TYPE_OBJECT);
198 
199 	g_type_class_add_private (gobject_class, sizeof (GnomeBGCrossfadePrivate));
200 }
201 
202 static void
gnome_bg_crossfade_init(GnomeBGCrossfade * fade)203 gnome_bg_crossfade_init (GnomeBGCrossfade *fade)
204 {
205 	fade->priv = GNOME_BG_CROSSFADE_GET_PRIVATE (fade);
206 
207 	fade->priv->fading_surface = NULL;
208 	fade->priv->end_surface = NULL;
209 	fade->priv->timeout_id = 0;
210 }
211 
212 /**
213  * gnome_bg_crossfade_new:
214  * @width: The width of the crossfading window
215  * @height: The height of the crossfading window
216  *
217  * Creates a new object to manage crossfading a
218  * window background between two #cairo_surface_ts.
219  *
220  * Return value: the new #GnomeBGCrossfade
221  **/
222 GnomeBGCrossfade *
gnome_bg_crossfade_new(int width,int height)223 gnome_bg_crossfade_new (int width,
224 			int height)
225 {
226 	GObject *object;
227 
228 	object = g_object_new (GNOME_TYPE_BG_CROSSFADE,
229 			       "width", width,
230 			       "height", height, NULL);
231 
232 	return (GnomeBGCrossfade *) object;
233 }
234 
235 static cairo_surface_t *
tile_surface(cairo_surface_t * surface,int width,int height)236 tile_surface (cairo_surface_t *surface,
237 	      int              width,
238 	      int              height)
239 {
240 	cairo_surface_t *copy;
241 	cairo_t *cr;
242 
243         if (surface == NULL) {
244                 copy = gdk_window_create_similar_surface (gdk_get_default_root_window (),
245                                                           CAIRO_CONTENT_COLOR,
246                                                           width, height);
247         } else {
248                 copy = cairo_surface_create_similar (surface,
249                                                      cairo_surface_get_content (surface),
250                                                      width, height);
251         }
252 
253 	cr = cairo_create (copy);
254 
255 	if (surface != NULL) {
256 		cairo_pattern_t *pattern;
257 		cairo_set_source_surface (cr, surface, 0.0, 0.0);
258 		pattern = cairo_get_source (cr);
259 		cairo_pattern_set_extend (pattern, CAIRO_EXTEND_REPEAT);
260 	} else {
261 		GtkStyle *style;
262 		style = gtk_widget_get_default_style ();
263 		gdk_cairo_set_source_color (cr, &style->bg[GTK_STATE_NORMAL]);
264 	}
265 
266 	cairo_paint (cr);
267 
268 	if (cairo_status (cr) != CAIRO_STATUS_SUCCESS) {
269 		cairo_surface_destroy (copy);
270 		copy = NULL;
271 	}
272 	cairo_destroy (cr);
273 
274 	return copy;
275 }
276 
277 /**
278  * gnome_bg_crossfade_set_start_surface:
279  * @fade: a #GnomeBGCrossfade
280  * @surface: The cairo surface to fade from
281  *
282  * Before initiating a crossfade with gnome_bg_crossfade_start()
283  * a start and end surface have to be set.  This function sets
284  * the surface shown at the beginning of the crossfade effect.
285  *
286  * Return value: %TRUE if successful, or %FALSE if the surface
287  * could not be copied.
288  **/
289 gboolean
gnome_bg_crossfade_set_start_surface(GnomeBGCrossfade * fade,cairo_surface_t * surface)290 gnome_bg_crossfade_set_start_surface (GnomeBGCrossfade *fade,
291 				      cairo_surface_t  *surface)
292 {
293 	g_return_val_if_fail (GNOME_IS_BG_CROSSFADE (fade), FALSE);
294 
295 	if (fade->priv->fading_surface != NULL) {
296 		cairo_surface_destroy (fade->priv->fading_surface);
297 		fade->priv->fading_surface = NULL;
298 	}
299 
300 	fade->priv->fading_surface = tile_surface (surface,
301 						   fade->priv->width,
302 						   fade->priv->height);
303 
304 	return fade->priv->fading_surface != NULL;
305 }
306 
307 static gdouble
get_current_time(void)308 get_current_time (void)
309 {
310 	const double microseconds_per_second = (double) G_USEC_PER_SEC;
311 	double timestamp;
312 	GTimeVal now;
313 
314 	g_get_current_time (&now);
315 
316 	timestamp = ((microseconds_per_second * now.tv_sec) + now.tv_usec) /
317 	            microseconds_per_second;
318 
319 	return timestamp;
320 }
321 
322 /**
323  * gnome_bg_crossfade_set_end_surface:
324  * @fade: a #GnomeBGCrossfade
325  * @surface: The cairo surface to fade to
326  *
327  * Before initiating a crossfade with gnome_bg_crossfade_start()
328  * a start and end surface have to be set.  This function sets
329  * the surface shown at the end of the crossfade effect.
330  *
331  * Return value: %TRUE if successful, or %FALSE if the surface
332  * could not be copied.
333  **/
334 gboolean
gnome_bg_crossfade_set_end_surface(GnomeBGCrossfade * fade,cairo_surface_t * surface)335 gnome_bg_crossfade_set_end_surface (GnomeBGCrossfade *fade,
336 				    cairo_surface_t  *surface)
337 {
338 	g_return_val_if_fail (GNOME_IS_BG_CROSSFADE (fade), FALSE);
339 
340 	if (fade->priv->end_surface != NULL) {
341 		cairo_surface_destroy (fade->priv->end_surface);
342 		fade->priv->end_surface = NULL;
343 	}
344 
345 	fade->priv->end_surface = tile_surface (surface,
346 					        fade->priv->width,
347 					        fade->priv->height);
348 
349 	/* Reset timer in case we're called while animating
350 	 */
351 	fade->priv->start_time = get_current_time ();
352 	return fade->priv->end_surface != NULL;
353 }
354 
355 static gboolean
animations_are_disabled(GnomeBGCrossfade * fade)356 animations_are_disabled (GnomeBGCrossfade *fade)
357 {
358 	GtkSettings *settings;
359 	GdkScreen *screen;
360 	gboolean are_enabled;
361 
362 	g_assert (fade->priv->window != NULL);
363 
364 	screen = gdk_window_get_screen (fade->priv->window);
365 
366 	settings = gtk_settings_get_for_screen (screen);
367 
368 	g_object_get (settings, "gtk-enable-animations", &are_enabled, NULL);
369 
370 	return !are_enabled;
371 }
372 
373 static void
send_root_property_change_notification(GnomeBGCrossfade * fade)374 send_root_property_change_notification (GnomeBGCrossfade *fade)
375 {
376         long zero_length_pixmap;
377 
378         /* We do a zero length append to force a change notification,
379          * without changing the value */
380         XChangeProperty (GDK_WINDOW_XDISPLAY (fade->priv->window),
381                          GDK_WINDOW_XID (fade->priv->window),
382                          gdk_x11_get_xatom_by_name ("_XROOTPMAP_ID"),
383                          XA_PIXMAP, 32, PropModeAppend,
384                          (guchar *) &zero_length_pixmap, 0);
385 }
386 
387 static void
draw_background(GnomeBGCrossfade * fade)388 draw_background (GnomeBGCrossfade *fade)
389 {
390 	if (gdk_window_get_window_type (fade->priv->window) == GDK_WINDOW_ROOT) {
391 		gdk_error_trap_push ();
392                 XClearArea (GDK_WINDOW_XDISPLAY (fade->priv->window),
393                             GDK_WINDOW_XID (fade->priv->window),
394                             0, 0,
395                             gdk_window_get_width (fade->priv->window),
396                             gdk_window_get_height (fade->priv->window),
397                             False);
398 
399                 send_root_property_change_notification (fade);
400 
401 		gdk_flush ();
402 		gdk_error_trap_pop (); // ignore errors
403 	} else {
404 		gdk_window_invalidate_rect (fade->priv->window, NULL, FALSE);
405 		gdk_window_process_updates (fade->priv->window, FALSE);
406 	}
407 }
408 
409 static gboolean
on_tick(GnomeBGCrossfade * fade)410 on_tick (GnomeBGCrossfade *fade)
411 {
412 	gdouble now, percent_done;
413 	cairo_t *cr;
414 	cairo_status_t status;
415 
416 	g_return_val_if_fail (GNOME_IS_BG_CROSSFADE (fade), FALSE);
417 
418 	now = get_current_time ();
419 
420 	percent_done = (now - fade->priv->start_time) / fade->priv->total_duration;
421 	percent_done = CLAMP (percent_done, 0.0, 1.0);
422 
423 	/* If it's taking a long time to get to the first frame,
424 	 * then lengthen the duration, so the user will get to see
425 	 * the effect.
426 	 */
427 	if (fade->priv->is_first_frame && percent_done > .33) {
428 		fade->priv->is_first_frame = FALSE;
429 		fade->priv->total_duration *= 1.5;
430 		return on_tick (fade);
431 	}
432 
433 	if (fade->priv->fading_surface == NULL) {
434 		return FALSE;
435 	}
436 
437 	if (animations_are_disabled (fade)) {
438 		return FALSE;
439 	}
440 
441 	/* We accumulate the results in place for performance reasons.
442 	 *
443 	 * This means 1) The fade is exponential, not linear (looks good!)
444 	 * 2) The rate of fade is not independent of frame rate. Slower machines
445 	 * will get a slower fade (but never longer than .75 seconds), and
446 	 * even the fastest machines will get *some* fade because the framerate
447 	 * is capped.
448 	 */
449 	cr = cairo_create (fade->priv->fading_surface);
450 
451 	cairo_set_source_surface (cr, fade->priv->end_surface,
452 				  0.0, 0.0);
453 	cairo_paint_with_alpha (cr, percent_done);
454 
455 	status = cairo_status (cr);
456 	cairo_destroy (cr);
457 
458 	if (status == CAIRO_STATUS_SUCCESS) {
459 		draw_background (fade);
460 	}
461 	return percent_done <= .99;
462 }
463 
464 static void
on_finished(GnomeBGCrossfade * fade)465 on_finished (GnomeBGCrossfade *fade)
466 {
467         cairo_pattern_t *pattern;
468 
469 	if (fade->priv->timeout_id == 0)
470 		return;
471 
472 	g_assert (fade->priv->end_surface != NULL);
473 
474         pattern = cairo_pattern_create_for_surface (fade->priv->end_surface);
475 	gdk_window_set_background_pattern (fade->priv->window, pattern);
476         cairo_pattern_destroy (pattern);
477 
478 	draw_background (fade);
479 
480 	cairo_surface_destroy (fade->priv->end_surface);
481 	fade->priv->end_surface = NULL;
482 
483 	g_assert (fade->priv->fading_surface != NULL);
484 
485 	cairo_surface_destroy (fade->priv->fading_surface);
486 	fade->priv->fading_surface = NULL;
487 
488 	fade->priv->timeout_id = 0;
489 	g_signal_emit (fade, signals[FINISHED], 0, fade->priv->window);
490 }
491 
492 /**
493  * gnome_bg_crossfade_start:
494  * @fade: a #GnomeBGCrossfade
495  * @window: The #GdkWindow to draw crossfade on
496  *
497  * This function initiates a quick crossfade between two surfaces on
498  * the background of @window.  Before initiating the crossfade both
499  * gnome_bg_crossfade_start() and gnome_bg_crossfade_end() need to
500  * be called. If animations are disabled, the crossfade is skipped,
501  * and the window background is set immediately to the end surface.
502  **/
503 void
gnome_bg_crossfade_start(GnomeBGCrossfade * fade,GdkWindow * window)504 gnome_bg_crossfade_start (GnomeBGCrossfade *fade,
505 			  GdkWindow        *window)
506 {
507 	GSource *source;
508 	GMainContext *context;
509         cairo_pattern_t *pattern;
510 
511 	g_return_if_fail (GNOME_IS_BG_CROSSFADE (fade));
512 	g_return_if_fail (window != NULL);
513 	g_return_if_fail (fade->priv->fading_surface != NULL);
514 	g_return_if_fail (fade->priv->end_surface != NULL);
515 	g_return_if_fail (!gnome_bg_crossfade_is_started (fade));
516 	g_return_if_fail (gdk_window_get_window_type (window) != GDK_WINDOW_FOREIGN);
517 
518 	source = g_timeout_source_new (1000 / 60.0);
519 	g_source_set_callback (source,
520 			       (GSourceFunc) on_tick,
521 			       fade,
522 			       (GDestroyNotify) on_finished);
523 	context = g_main_context_default ();
524 	fade->priv->timeout_id = g_source_attach (source, context);
525 	g_source_unref (source);
526 
527 	fade->priv->window = window;
528         pattern = cairo_pattern_create_for_surface (fade->priv->fading_surface);
529 	gdk_window_set_background_pattern (fade->priv->window, pattern);
530         cairo_pattern_destroy (pattern);
531 
532 	draw_background (fade);
533 
534 	fade->priv->is_first_frame = TRUE;
535 	fade->priv->total_duration = .75;
536 	fade->priv->start_time = get_current_time ();
537 }
538 
539 
540 /**
541  * gnome_bg_crossfade_is_started:
542  * @fade: a #GnomeBGCrossfade
543  *
544  * This function reveals whether or not @fade is currently
545  * running on a window.  See gnome_bg_crossfade_start() for
546  * information on how to initiate a crossfade.
547  *
548  * Return value: %TRUE if fading, or %FALSE if not fading
549  **/
550 gboolean
gnome_bg_crossfade_is_started(GnomeBGCrossfade * fade)551 gnome_bg_crossfade_is_started (GnomeBGCrossfade *fade)
552 {
553 	g_return_val_if_fail (GNOME_IS_BG_CROSSFADE (fade), FALSE);
554 
555 	return fade->priv->timeout_id != 0;
556 }
557 
558 /**
559  * gnome_bg_crossfade_stop:
560  * @fade: a #GnomeBGCrossfade
561  *
562  * This function stops any in progress crossfades that may be
563  * happening.  It's harmless to call this function if @fade is
564  * already stopped.
565  **/
566 void
gnome_bg_crossfade_stop(GnomeBGCrossfade * fade)567 gnome_bg_crossfade_stop (GnomeBGCrossfade *fade)
568 {
569 	g_return_if_fail (GNOME_IS_BG_CROSSFADE (fade));
570 
571 	if (!gnome_bg_crossfade_is_started (fade))
572 		return;
573 
574 	g_assert (fade->priv->timeout_id != 0);
575 	g_source_remove (fade->priv->timeout_id);
576 	fade->priv->timeout_id = 0;
577 }
578