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