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