1 /* GTK - The GIMP Toolkit
2  * Copyright (C) 2013 Red Hat, Inc.
3  *
4  * This library is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU Lesser General Public
6  * License as published by the Free Software Foundation; either
7  * version 2 of the License, or (at your option) any later version.
8  *
9  * This library is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12  * Lesser General Public License for more details.
13  *
14  * You should have received a copy of the GNU Lesser General Public
15  * License along with this library. If not, see <http://www.gnu.org/licenses/>.
16  */
17 
18 #include "config.h"
19 
20 #include "gtkdebug.h"
21 #include "gtkprivate.h"
22 #include "gtkpixelcacheprivate.h"
23 #include "gtkrenderbackgroundprivate.h"
24 #include "gtkstylecontextprivate.h"
25 
26 #define BLOW_CACHE_TIMEOUT_SEC 20
27 
28 /* The extra size of the offscreen surface we allocate
29    to make scrolling more efficient */
30 #define DEFAULT_EXTRA_SIZE 64
31 
32 /* When resizing viewport we allow this extra
33    size to avoid constantly reallocating when resizing */
34 #define ALLOW_SMALLER_SIZE_FACTOR 0.5
35 #define ALLOW_LARGER_SIZE_FACTOR  1.25
36 
37 struct _GtkPixelCache {
38   cairo_surface_t *surface;
39   cairo_content_t content;
40 
41   /* Valid if surface != NULL */
42   int surface_x;
43   int surface_y;
44   int surface_w;
45   int surface_h;
46   double surface_scale;
47 
48   /* may be null if not dirty */
49   cairo_region_t *surface_dirty;
50 
51   GSource *timeout_source;
52 
53   guint extra_width;
54   guint extra_height;
55 
56   guint always_cache : 1;
57   guint is_opaque : 1;
58 };
59 
60 GtkPixelCache *
_gtk_pixel_cache_new(void)61 _gtk_pixel_cache_new (void)
62 {
63   GtkPixelCache *cache;
64 
65   cache = g_new0 (GtkPixelCache, 1);
66   cache->extra_width = DEFAULT_EXTRA_SIZE;
67   cache->extra_height = DEFAULT_EXTRA_SIZE;
68 
69   return cache;
70 }
71 
72 void
_gtk_pixel_cache_free(GtkPixelCache * cache)73 _gtk_pixel_cache_free (GtkPixelCache *cache)
74 {
75   if (cache == NULL)
76     return;
77 
78   if (cache->timeout_source ||
79       cache->surface ||
80       cache->surface_dirty)
81     {
82       g_warning ("pixel cache freed that wasn't unmapped: tag %u surface %p dirty %p",
83                  g_source_get_id (cache->timeout_source), cache->surface, cache->surface_dirty);
84     }
85 
86   g_clear_pointer (&cache->timeout_source, g_source_destroy);
87   g_clear_pointer (&cache->surface, cairo_surface_destroy);
88   g_clear_pointer (&cache->surface_dirty, cairo_region_destroy);
89 
90   g_free (cache);
91 }
92 
93 void
_gtk_pixel_cache_set_extra_size(GtkPixelCache * cache,guint extra_width,guint extra_height)94 _gtk_pixel_cache_set_extra_size (GtkPixelCache *cache,
95                                  guint          extra_width,
96                                  guint          extra_height)
97 {
98   cache->extra_width = extra_width ? extra_width : DEFAULT_EXTRA_SIZE;
99   cache->extra_height = extra_height ? extra_height : DEFAULT_EXTRA_SIZE;
100 }
101 
102 void
_gtk_pixel_cache_get_extra_size(GtkPixelCache * cache,guint * extra_width,guint * extra_height)103 _gtk_pixel_cache_get_extra_size (GtkPixelCache *cache,
104                                  guint         *extra_width,
105                                  guint         *extra_height)
106 {
107   if (extra_width)
108     *extra_width = cache->extra_width;
109 
110   if (extra_height)
111     *extra_height = cache->extra_height;
112 }
113 
114 void
_gtk_pixel_cache_set_content(GtkPixelCache * cache,cairo_content_t content)115 _gtk_pixel_cache_set_content (GtkPixelCache   *cache,
116                               cairo_content_t  content)
117 {
118   cache->content = content;
119   _gtk_pixel_cache_invalidate (cache, NULL);
120 }
121 
122 /* Region is in canvas coordinates */
123 void
_gtk_pixel_cache_invalidate(GtkPixelCache * cache,cairo_region_t * region)124 _gtk_pixel_cache_invalidate (GtkPixelCache  *cache,
125                              cairo_region_t *region)
126 {
127   cairo_rectangle_int_t r;
128   cairo_region_t *free_region = NULL;
129 
130   if (cache->surface == NULL ||
131       (region != NULL && cairo_region_is_empty (region)))
132     return;
133 
134   if (region == NULL)
135     {
136       r.x = cache->surface_x;
137       r.y = cache->surface_y;
138       r.width = cache->surface_w;
139       r.height = cache->surface_h;
140 
141       free_region = region =
142         cairo_region_create_rectangle (&r);
143     }
144 
145   if (cache->surface_dirty == NULL)
146     {
147       cache->surface_dirty = cairo_region_copy (region);
148       cairo_region_translate (cache->surface_dirty,
149                               -cache->surface_x,
150                               -cache->surface_y);
151     }
152   else
153     {
154       cairo_region_translate (region,
155                               -cache->surface_x,
156                               -cache->surface_y);
157       cairo_region_union (cache->surface_dirty, region);
158       cairo_region_translate (region,
159                               cache->surface_x,
160                               cache->surface_y);
161     }
162 
163   if (free_region)
164     cairo_region_destroy (free_region);
165 
166   r.x = 0;
167   r.y = 0;
168   r.width = cache->surface_w;
169   r.height = cache->surface_h;
170 
171   cairo_region_intersect_rectangle (cache->surface_dirty, &r);
172 }
173 
174 static void
_gtk_pixel_cache_create_surface_if_needed(GtkPixelCache * cache,GdkWindow * window,cairo_rectangle_int_t * view_rect,cairo_rectangle_int_t * canvas_rect)175 _gtk_pixel_cache_create_surface_if_needed (GtkPixelCache         *cache,
176                                            GdkWindow             *window,
177                                            cairo_rectangle_int_t *view_rect,
178                                            cairo_rectangle_int_t *canvas_rect)
179 {
180   cairo_rectangle_int_t rect;
181   int surface_w, surface_h;
182   cairo_content_t content;
183 
184 #ifdef G_ENABLE_DEBUG
185   if (GTK_DISPLAY_DEBUG_CHECK (gdk_window_get_display (window), NO_PIXEL_CACHE))
186     return;
187 #endif
188 
189   content = cache->content;
190   if (!content)
191     {
192       if (cache->is_opaque)
193         content = CAIRO_CONTENT_COLOR;
194       else
195         content = CAIRO_CONTENT_COLOR_ALPHA;
196     }
197 
198   surface_w = view_rect->width;
199   if (canvas_rect->width > surface_w)
200     surface_w = MIN (surface_w + cache->extra_width, canvas_rect->width);
201 
202   surface_h = view_rect->height;
203   if (canvas_rect->height > surface_h)
204     surface_h = MIN (surface_h + cache->extra_height, canvas_rect->height);
205 
206   /* If current surface can't fit view_rect or is too large, kill it */
207   if (cache->surface != NULL &&
208       (cairo_surface_get_content (cache->surface) != content ||
209        cache->surface_w < MAX(view_rect->width, surface_w * ALLOW_SMALLER_SIZE_FACTOR) ||
210        cache->surface_w > surface_w * ALLOW_LARGER_SIZE_FACTOR ||
211        cache->surface_h < MAX(view_rect->height, surface_h * ALLOW_SMALLER_SIZE_FACTOR) ||
212        cache->surface_h > surface_h * ALLOW_LARGER_SIZE_FACTOR ||
213        cache->surface_scale != gdk_window_get_scale_factor (window)))
214     {
215       cairo_surface_destroy (cache->surface);
216       cache->surface = NULL;
217       if (cache->surface_dirty)
218         cairo_region_destroy (cache->surface_dirty);
219       cache->surface_dirty = NULL;
220     }
221 
222   /* Don't allocate a surface if view >= canvas, as we won't
223    * be scrolling then anyway, unless the widget requested it.
224    */
225   if (cache->surface == NULL &&
226       (cache->always_cache ||
227        (view_rect->width < canvas_rect->width ||
228         view_rect->height < canvas_rect->height)))
229     {
230       cache->surface_x = -canvas_rect->x;
231       cache->surface_y = -canvas_rect->y;
232       cache->surface_w = MAX (surface_w, 1);
233       cache->surface_h = MAX (surface_h, 1);
234       cache->surface_scale = gdk_window_get_scale_factor (window);
235 
236       cache->surface =
237         gdk_window_create_similar_surface (window, content,
238                                            cache->surface_w,
239                                            cache->surface_h);
240       rect.x = 0;
241       rect.y = 0;
242       rect.width = cache->surface_w;
243       rect.height = cache->surface_h;
244       cache->surface_dirty =
245         cairo_region_create_rectangle (&rect);
246     }
247 }
248 
249 static void
_gtk_pixel_cache_set_position(GtkPixelCache * cache,cairo_rectangle_int_t * view_rect,cairo_rectangle_int_t * canvas_rect)250 _gtk_pixel_cache_set_position (GtkPixelCache         *cache,
251                                cairo_rectangle_int_t *view_rect,
252                                cairo_rectangle_int_t *canvas_rect)
253 {
254   cairo_rectangle_int_t r, view_pos;
255   cairo_region_t *copy_region;
256   int new_surf_x, new_surf_y;
257   cairo_t *backing_cr;
258 
259   if (cache->surface == NULL)
260     return;
261 
262   /* Position of view inside canvas */
263   view_pos.x = -canvas_rect->x;
264   view_pos.y = -canvas_rect->y;
265   view_pos.width = view_rect->width;
266   view_pos.height = view_rect->height;
267 
268   /* Reposition so all is visible */
269   if (view_pos.x < cache->surface_x ||
270       view_pos.x + view_pos.width > cache->surface_x + cache->surface_w ||
271       view_pos.y < cache->surface_y ||
272       view_pos.y + view_pos.height > cache->surface_y + cache->surface_h)
273     {
274       new_surf_x = cache->surface_x;
275       if (view_pos.x < cache->surface_x)
276         new_surf_x = MAX (view_pos.x + view_pos.width - cache->surface_w, 0);
277       else if (view_pos.x + view_pos.width >
278                cache->surface_x + cache->surface_w)
279         new_surf_x = MIN (view_pos.x, canvas_rect->width - cache->surface_w);
280 
281       new_surf_y = cache->surface_y;
282       if (view_pos.y < cache->surface_y)
283         new_surf_y = MAX (view_pos.y + view_pos.height - cache->surface_h, 0);
284       else if (view_pos.y + view_pos.height >
285                cache->surface_y + cache->surface_h)
286         new_surf_y = MIN (view_pos.y, canvas_rect->height - cache->surface_h);
287 
288       r.x = 0;
289       r.y = 0;
290       r.width = cache->surface_w;
291       r.height = cache->surface_h;
292       copy_region = cairo_region_create_rectangle (&r);
293 
294       if (cache->surface_dirty)
295         {
296           cairo_region_subtract (copy_region, cache->surface_dirty);
297           cairo_region_destroy (cache->surface_dirty);
298           cache->surface_dirty = NULL;
299         }
300 
301       cairo_region_translate (copy_region,
302                               cache->surface_x - new_surf_x,
303                               cache->surface_y - new_surf_y);
304       cairo_region_intersect_rectangle (copy_region, &r);
305 
306       backing_cr = cairo_create (cache->surface);
307       gdk_cairo_region (backing_cr, copy_region);
308       cairo_set_operator (backing_cr, CAIRO_OPERATOR_SOURCE);
309       cairo_clip (backing_cr);
310       cairo_push_group (backing_cr);
311       cairo_set_source_surface (backing_cr, cache->surface,
312                                 cache->surface_x - new_surf_x,
313                                 cache->surface_y - new_surf_y);
314       cairo_paint (backing_cr);
315       cairo_pop_group_to_source (backing_cr);
316       cairo_paint (backing_cr);
317       cairo_destroy (backing_cr);
318 
319       cache->surface_x = new_surf_x;
320       cache->surface_y = new_surf_y;
321 
322       cairo_region_xor_rectangle (copy_region, &r);
323       cache->surface_dirty = copy_region;
324     }
325 }
326 
327 static void
_gtk_pixel_cache_repaint(GtkPixelCache * cache,GdkWindow * window,GtkPixelCacheDrawFunc draw,cairo_rectangle_int_t * view_rect,cairo_rectangle_int_t * canvas_rect,gpointer user_data)328 _gtk_pixel_cache_repaint (GtkPixelCache         *cache,
329                           GdkWindow             *window,
330                           GtkPixelCacheDrawFunc  draw,
331                           cairo_rectangle_int_t *view_rect,
332                           cairo_rectangle_int_t *canvas_rect,
333                           gpointer               user_data)
334 {
335   cairo_t *backing_cr;
336   cairo_region_t *region_dirty = cache->surface_dirty;
337   cache->surface_dirty = NULL;
338 
339   if (cache->surface &&
340       region_dirty &&
341       !cairo_region_is_empty (region_dirty))
342     {
343       backing_cr = cairo_create (cache->surface);
344       gdk_cairo_region (backing_cr, region_dirty);
345       cairo_clip (backing_cr);
346       cairo_translate (backing_cr,
347                        -cache->surface_x - canvas_rect->x - view_rect->x,
348                        -cache->surface_y - canvas_rect->y - view_rect->y);
349 
350       cairo_save (backing_cr);
351       cairo_set_source_rgba (backing_cr,
352                              0.0, 0, 0, 0.0);
353       cairo_set_operator (backing_cr, CAIRO_OPERATOR_SOURCE);
354       cairo_paint (backing_cr);
355       cairo_restore (backing_cr);
356 
357       cairo_save (backing_cr);
358       draw (backing_cr, user_data);
359       cairo_restore (backing_cr);
360 
361 #ifdef G_ENABLE_DEBUG
362       if (GTK_DISPLAY_DEBUG_CHECK (gdk_window_get_display (window), PIXEL_CACHE))
363         {
364           GdkRGBA colors[] = {
365             { 1, 0, 0, 0.08},
366             { 0, 1, 0, 0.08},
367             { 0, 0, 1, 0.08},
368             { 1, 0, 1, 0.08},
369             { 1, 1, 0, 0.08},
370             { 0, 1, 1, 0.08},
371           };
372           static int current_color = 0;
373 
374           gdk_cairo_set_source_rgba (backing_cr, &colors[(current_color++) % G_N_ELEMENTS (colors)]);
375           cairo_paint (backing_cr);
376         }
377 #endif
378 
379       cairo_destroy (backing_cr);
380     }
381 
382   if (region_dirty)
383     cairo_region_destroy (region_dirty);
384 }
385 
386 static void
gtk_pixel_cache_blow_cache(GtkPixelCache * cache)387 gtk_pixel_cache_blow_cache (GtkPixelCache *cache)
388 {
389   g_clear_pointer (&cache->timeout_source, g_source_destroy);
390   g_clear_pointer (&cache->surface, cairo_surface_destroy);
391   g_clear_pointer (&cache->surface_dirty, cairo_region_destroy);
392 }
393 
394 static gboolean
blow_cache_cb(gpointer user_data)395 blow_cache_cb  (gpointer user_data)
396 {
397   GtkPixelCache *cache = user_data;
398 
399   cache->timeout_source = NULL;
400 
401   gtk_pixel_cache_blow_cache (cache);
402 
403   return G_SOURCE_REMOVE;
404 }
405 
406 static gboolean
context_is_unscaled(cairo_t * cr)407 context_is_unscaled (cairo_t *cr)
408 {
409   cairo_matrix_t matrix;
410   gdouble x, y;
411 
412   x = y = 1;
413   cairo_get_matrix (cr, &matrix);
414   cairo_matrix_transform_distance (&matrix, &x, &y);
415 
416   return x == 1 && y == 1;
417 }
418 
419 
420 void
_gtk_pixel_cache_draw(GtkPixelCache * cache,cairo_t * cr,GdkWindow * window,cairo_rectangle_int_t * view_rect,cairo_rectangle_int_t * canvas_rect,GtkPixelCacheDrawFunc draw,gpointer user_data)421 _gtk_pixel_cache_draw (GtkPixelCache         *cache,
422                        cairo_t               *cr,
423                        GdkWindow             *window,
424                        cairo_rectangle_int_t *view_rect,   /* View position in widget coords */
425                        cairo_rectangle_int_t *canvas_rect, /* Size and position of canvas in view coords */
426                        GtkPixelCacheDrawFunc  draw,
427                        gpointer               user_data)
428 {
429   if (cache->timeout_source)
430     {
431       gint64 deadline;
432 
433       deadline = g_get_monotonic_time () + (BLOW_CACHE_TIMEOUT_SEC * G_USEC_PER_SEC);
434       g_source_set_ready_time (cache->timeout_source, deadline);
435     }
436   else
437     {
438       guint tag;
439 
440       tag = g_timeout_add_seconds (BLOW_CACHE_TIMEOUT_SEC, blow_cache_cb, cache);
441       cache->timeout_source = g_main_context_find_source_by_id (NULL, tag);
442       g_source_set_name (cache->timeout_source, "[gtk+] blow_cache_cb");
443     }
444 
445   _gtk_pixel_cache_create_surface_if_needed (cache, window,
446                                              view_rect, canvas_rect);
447   _gtk_pixel_cache_set_position (cache, view_rect, canvas_rect);
448   _gtk_pixel_cache_repaint (cache, window, draw, view_rect, canvas_rect, user_data);
449 
450   if (cache->surface && context_is_unscaled (cr) &&
451       /* Don't use backing surface if rendering elsewhere */
452       cairo_surface_get_type (cache->surface) == cairo_surface_get_type (cairo_get_target (cr)))
453     {
454       cairo_save (cr);
455       cairo_set_source_surface (cr, cache->surface,
456                                 cache->surface_x + view_rect->x + canvas_rect->x,
457                                 cache->surface_y + view_rect->y + canvas_rect->y);
458       cairo_rectangle (cr, view_rect->x, view_rect->y,
459                        view_rect->width, view_rect->height);
460       cairo_fill (cr);
461       cairo_restore (cr);
462     }
463   else
464     {
465       cairo_rectangle (cr,
466                        view_rect->x, view_rect->y,
467                        view_rect->width, view_rect->height);
468       cairo_clip (cr);
469       draw (cr, user_data);
470     }
471 }
472 
473 void
_gtk_pixel_cache_map(GtkPixelCache * cache)474 _gtk_pixel_cache_map (GtkPixelCache *cache)
475 {
476   _gtk_pixel_cache_invalidate (cache, NULL);
477 }
478 
479 void
_gtk_pixel_cache_unmap(GtkPixelCache * cache)480 _gtk_pixel_cache_unmap (GtkPixelCache *cache)
481 {
482   gtk_pixel_cache_blow_cache (cache);
483 }
484 
485 gboolean
_gtk_pixel_cache_get_always_cache(GtkPixelCache * cache)486 _gtk_pixel_cache_get_always_cache (GtkPixelCache *cache)
487 {
488   return cache->always_cache;
489 }
490 
491 void
_gtk_pixel_cache_set_always_cache(GtkPixelCache * cache,gboolean always_cache)492 _gtk_pixel_cache_set_always_cache (GtkPixelCache *cache,
493                                    gboolean       always_cache)
494 {
495   cache->always_cache = !!always_cache;
496 }
497 
498 void
gtk_pixel_cache_set_is_opaque(GtkPixelCache * cache,gboolean is_opaque)499 gtk_pixel_cache_set_is_opaque (GtkPixelCache *cache,
500                                gboolean       is_opaque)
501 {
502   if (cache->is_opaque == is_opaque)
503     return;
504 
505   cache->is_opaque = is_opaque;
506   _gtk_pixel_cache_invalidate (cache, NULL);
507 }
508