1 | /* |
2 | * Copyright © 2018 Benjamin Otte |
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.1 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 | * Authors: Benjamin Otte <otte@gnome.org> |
18 | */ |
19 | |
20 | #include "config.h" |
21 | |
22 | #include "gtkwidgetpaintableprivate.h" |
23 | |
24 | #include "gtkintl.h" |
25 | #include "gtksnapshot.h" |
26 | #include "gtkrendernodepaintableprivate.h" |
27 | #include "gtkwidgetprivate.h" |
28 | |
29 | /** |
30 | * GtkWidgetPaintable: |
31 | * |
32 | * `GtkWidgetPaintable` is a `GdkPaintable` that displays the contents |
33 | * of a widget. |
34 | * |
35 | * `GtkWidgetPaintable` will also take care of the widget not being in a |
36 | * state where it can be drawn (like when it isn't shown) and just draw |
37 | * nothing or where it does not have a size (like when it is hidden) and |
38 | * report no size in that case. |
39 | * |
40 | * Of course, `GtkWidgetPaintable` allows you to monitor widgets for size |
41 | * changes by emitting the [signal@Gdk.Paintable::invalidate-size] signal |
42 | * whenever the size of the widget changes as well as for visual changes by |
43 | * emitting the [signal@Gdk.Paintable::invalidate-contents] signal whenever |
44 | * the widget changes. |
45 | * |
46 | * You can use a `GtkWidgetPaintable` everywhere a `GdkPaintable` is allowed, |
47 | * including using it on a `GtkPicture` (or one of its parents) that it was |
48 | * set on itself via gtk_picture_set_paintable(). The paintable will take care |
49 | * of recursion when this happens. If you do this however, ensure that the |
50 | * [property@Gtk.Picture:can-shrink] property is set to %TRUE or you might |
51 | * end up with an infinitely growing widget. |
52 | */ |
53 | struct _GtkWidgetPaintable |
54 | { |
55 | GObject parent_instance; |
56 | |
57 | GtkWidget *widget; |
58 | guint snapshot_count; |
59 | |
60 | guint pending_update_cb; /* the idle source that updates the valid image to be the new current image */ |
61 | |
62 | GdkPaintable *current_image; /* the image that we are presenting */ |
63 | GdkPaintable *pending_image; /* the image that we should be presenting */ |
64 | }; |
65 | |
66 | struct _GtkWidgetPaintableClass |
67 | { |
68 | GObjectClass parent_class; |
69 | }; |
70 | |
71 | enum { |
72 | PROP_0, |
73 | PROP_WIDGET, |
74 | |
75 | N_PROPS, |
76 | }; |
77 | |
78 | static GParamSpec *properties[N_PROPS] = { NULL, }; |
79 | |
80 | static void |
81 | gtk_widget_paintable_paintable_snapshot (GdkPaintable *paintable, |
82 | GdkSnapshot *snapshot, |
83 | double width, |
84 | double height) |
85 | { |
86 | GtkWidgetPaintable *self = GTK_WIDGET_PAINTABLE (ptr: paintable); |
87 | |
88 | if (self->snapshot_count > 4) |
89 | return; |
90 | else if (self->snapshot_count > 0) |
91 | { |
92 | graphene_rect_t bounds; |
93 | |
94 | gtk_snapshot_push_clip (snapshot, |
95 | bounds: &GRAPHENE_RECT_INIT(0, 0, width, height)); |
96 | |
97 | if (gtk_widget_compute_bounds (widget: self->widget, target: self->widget, out_bounds: &bounds)) |
98 | { |
99 | gtk_snapshot_scale (snapshot, factor_x: width / bounds.size.width, factor_y: height / bounds.size.height); |
100 | gtk_snapshot_translate (snapshot, point: &bounds.origin); |
101 | } |
102 | |
103 | gtk_widget_snapshot (widget: self->widget, snapshot); |
104 | |
105 | gtk_snapshot_pop (snapshot); |
106 | } |
107 | else |
108 | { |
109 | gdk_paintable_snapshot (paintable: self->current_image, snapshot, width, height); |
110 | } |
111 | } |
112 | |
113 | static GdkPaintable * |
114 | gtk_widget_paintable_paintable_get_current_image (GdkPaintable *paintable) |
115 | { |
116 | GtkWidgetPaintable *self = GTK_WIDGET_PAINTABLE (ptr: paintable); |
117 | |
118 | return g_object_ref (self->current_image); |
119 | } |
120 | |
121 | static int |
122 | gtk_widget_paintable_paintable_get_intrinsic_width (GdkPaintable *paintable) |
123 | { |
124 | GtkWidgetPaintable *self = GTK_WIDGET_PAINTABLE (ptr: paintable); |
125 | |
126 | return gdk_paintable_get_intrinsic_width (paintable: self->current_image); |
127 | } |
128 | |
129 | static int |
130 | gtk_widget_paintable_paintable_get_intrinsic_height (GdkPaintable *paintable) |
131 | { |
132 | GtkWidgetPaintable *self = GTK_WIDGET_PAINTABLE (ptr: paintable); |
133 | |
134 | return gdk_paintable_get_intrinsic_height (paintable: self->current_image); |
135 | } |
136 | |
137 | static void |
138 | gtk_widget_paintable_paintable_init (GdkPaintableInterface *iface) |
139 | { |
140 | iface->snapshot = gtk_widget_paintable_paintable_snapshot; |
141 | iface->get_current_image = gtk_widget_paintable_paintable_get_current_image; |
142 | iface->get_intrinsic_width = gtk_widget_paintable_paintable_get_intrinsic_width; |
143 | iface->get_intrinsic_height = gtk_widget_paintable_paintable_get_intrinsic_height; |
144 | } |
145 | |
146 | G_DEFINE_TYPE_EXTENDED (GtkWidgetPaintable, gtk_widget_paintable, G_TYPE_OBJECT, 0, |
147 | G_IMPLEMENT_INTERFACE (GDK_TYPE_PAINTABLE, |
148 | gtk_widget_paintable_paintable_init)) |
149 | |
150 | static void |
151 | gtk_widget_paintable_set_property (GObject *object, |
152 | guint prop_id, |
153 | const GValue *value, |
154 | GParamSpec *pspec) |
155 | |
156 | { |
157 | GtkWidgetPaintable *self = GTK_WIDGET_PAINTABLE (ptr: object); |
158 | |
159 | switch (prop_id) |
160 | { |
161 | case PROP_WIDGET: |
162 | gtk_widget_paintable_set_widget (self, widget: g_value_get_object (value)); |
163 | break; |
164 | |
165 | default: |
166 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
167 | break; |
168 | } |
169 | } |
170 | |
171 | static void |
172 | gtk_widget_paintable_get_property (GObject *object, |
173 | guint prop_id, |
174 | GValue *value, |
175 | GParamSpec *pspec) |
176 | { |
177 | GtkWidgetPaintable *self = GTK_WIDGET_PAINTABLE (ptr: object); |
178 | |
179 | switch (prop_id) |
180 | { |
181 | case PROP_WIDGET: |
182 | g_value_set_object (value, v_object: self->widget); |
183 | break; |
184 | |
185 | default: |
186 | G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); |
187 | break; |
188 | } |
189 | } |
190 | |
191 | static void |
192 | gtk_widget_paintable_unset_widget (GtkWidgetPaintable *self) |
193 | { |
194 | if (self->widget == NULL) |
195 | return; |
196 | |
197 | self->widget->priv->paintables = g_slist_remove (list: self->widget->priv->paintables, |
198 | data: self); |
199 | |
200 | self->widget = NULL; |
201 | |
202 | g_clear_object (&self->pending_image); |
203 | if (self->pending_update_cb) |
204 | { |
205 | g_source_remove (tag: self->pending_update_cb); |
206 | self->pending_update_cb = 0; |
207 | } |
208 | } |
209 | |
210 | static void |
211 | gtk_widget_paintable_dispose (GObject *object) |
212 | { |
213 | GtkWidgetPaintable *self = GTK_WIDGET_PAINTABLE (ptr: object); |
214 | |
215 | gtk_widget_paintable_unset_widget (self); |
216 | |
217 | G_OBJECT_CLASS (gtk_widget_paintable_parent_class)->dispose (object); |
218 | } |
219 | |
220 | static void |
221 | gtk_widget_paintable_finalize (GObject *object) |
222 | { |
223 | GtkWidgetPaintable *self = GTK_WIDGET_PAINTABLE (ptr: object); |
224 | |
225 | g_object_unref (object: self->current_image); |
226 | |
227 | G_OBJECT_CLASS (gtk_widget_paintable_parent_class)->finalize (object); |
228 | } |
229 | |
230 | static void |
231 | gtk_widget_paintable_class_init (GtkWidgetPaintableClass *klass) |
232 | { |
233 | GObjectClass *gobject_class = G_OBJECT_CLASS (klass); |
234 | |
235 | gobject_class->get_property = gtk_widget_paintable_get_property; |
236 | gobject_class->set_property = gtk_widget_paintable_set_property; |
237 | gobject_class->dispose = gtk_widget_paintable_dispose; |
238 | gobject_class->finalize = gtk_widget_paintable_finalize; |
239 | |
240 | /** |
241 | * GtkWidgetPaintable:widget: (attributes org.gtk.Property.get=gtk_widget_paintable_get_widget org.gtk.Property.set=gtk_widget_paintable_set_widget) |
242 | * |
243 | * The observed widget or %NULL if none. |
244 | */ |
245 | properties[PROP_WIDGET] = |
246 | g_param_spec_object (name: "widget" , |
247 | P_("Widget" ), |
248 | P_("Observed widget" ), |
249 | GTK_TYPE_WIDGET, |
250 | flags: G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); |
251 | |
252 | g_object_class_install_properties (oclass: gobject_class, n_pspecs: N_PROPS, pspecs: properties); |
253 | } |
254 | |
255 | static void |
256 | gtk_widget_paintable_init (GtkWidgetPaintable *self) |
257 | { |
258 | self->current_image = gdk_paintable_new_empty (intrinsic_width: 0, intrinsic_height: 0); |
259 | } |
260 | |
261 | /** |
262 | * gtk_widget_paintable_new: |
263 | * @widget: (nullable) (transfer none): a `GtkWidget` |
264 | * |
265 | * Creates a new widget paintable observing the given widget. |
266 | * |
267 | * Returns: (transfer full) (type GtkWidgetPaintable): a new `GtkWidgetPaintable` |
268 | */ |
269 | GdkPaintable * |
270 | gtk_widget_paintable_new (GtkWidget *widget) |
271 | { |
272 | g_return_val_if_fail (widget == NULL || GTK_IS_WIDGET (widget), NULL); |
273 | |
274 | return g_object_new (GTK_TYPE_WIDGET_PAINTABLE, |
275 | first_property_name: "widget" , widget, |
276 | NULL); |
277 | } |
278 | |
279 | static GdkPaintable * |
280 | gtk_widget_paintable_snapshot_widget (GtkWidgetPaintable *self) |
281 | { |
282 | graphene_rect_t bounds; |
283 | |
284 | if (self->widget == NULL) |
285 | return gdk_paintable_new_empty (intrinsic_width: 0, intrinsic_height: 0); |
286 | |
287 | if (!gtk_widget_compute_bounds (widget: self->widget, target: self->widget, out_bounds: &bounds)) |
288 | return gdk_paintable_new_empty (intrinsic_width: 0, intrinsic_height: 0); |
289 | |
290 | if (self->widget->priv->render_node == NULL) |
291 | return gdk_paintable_new_empty (intrinsic_width: bounds.size.width, intrinsic_height: bounds.size.height); |
292 | |
293 | return gtk_render_node_paintable_new (node: self->widget->priv->render_node, bounds: &bounds); |
294 | } |
295 | |
296 | /** |
297 | * gtk_widget_paintable_get_widget: (attributes org.gtk.Method.get_property=widget) |
298 | * @self: a `GtkWidgetPaintable` |
299 | * |
300 | * Returns the widget that is observed or %NULL if none. |
301 | * |
302 | * Returns: (transfer none) (nullable): the observed widget. |
303 | */ |
304 | GtkWidget * |
305 | gtk_widget_paintable_get_widget (GtkWidgetPaintable *self) |
306 | { |
307 | g_return_val_if_fail (GTK_IS_WIDGET_PAINTABLE (self), NULL); |
308 | |
309 | return self->widget; |
310 | } |
311 | |
312 | /** |
313 | * gtk_widget_paintable_set_widget: (attributes org.gtk.Method.set_property=widget) |
314 | * @self: a `GtkWidgetPaintable` |
315 | * @widget: (nullable): the widget to observe |
316 | * |
317 | * Sets the widget that should be observed. |
318 | */ |
319 | void |
320 | gtk_widget_paintable_set_widget (GtkWidgetPaintable *self, |
321 | GtkWidget *widget) |
322 | { |
323 | |
324 | g_return_if_fail (GTK_IS_WIDGET_PAINTABLE (self)); |
325 | g_return_if_fail (widget == NULL || GTK_IS_WIDGET (widget)); |
326 | |
327 | if (self->widget == widget) |
328 | return; |
329 | |
330 | gtk_widget_paintable_unset_widget (self); |
331 | |
332 | /* We do not ref the widget to not cause ref cycles when a widget |
333 | * is told to observe itself or one of its parent. |
334 | */ |
335 | self->widget = widget; |
336 | |
337 | if (widget) |
338 | widget->priv->paintables = g_slist_prepend (list: widget->priv->paintables, data: self); |
339 | |
340 | g_object_unref (object: self->current_image); |
341 | self->current_image = gtk_widget_paintable_snapshot_widget (self); |
342 | |
343 | g_object_notify_by_pspec (G_OBJECT (self), pspec: properties[PROP_WIDGET]); |
344 | gdk_paintable_invalidate_size (paintable: GDK_PAINTABLE (ptr: self)); |
345 | gdk_paintable_invalidate_contents (paintable: GDK_PAINTABLE (ptr: self)); |
346 | } |
347 | |
348 | static gboolean |
349 | gtk_widget_paintable_update_func (gpointer data) |
350 | { |
351 | GtkWidgetPaintable *self = data; |
352 | GdkPaintable *old_image; |
353 | |
354 | if (self->current_image != self->pending_image) |
355 | { |
356 | old_image = self->current_image; |
357 | self->current_image = self->pending_image; |
358 | self->pending_image = NULL; |
359 | self->pending_update_cb = 0; |
360 | |
361 | if (gdk_paintable_get_intrinsic_width (paintable: self->current_image) != gdk_paintable_get_intrinsic_width (paintable: old_image) || |
362 | gdk_paintable_get_intrinsic_height (paintable: self->current_image) != gdk_paintable_get_intrinsic_height (paintable: old_image)) |
363 | gdk_paintable_invalidate_size (paintable: GDK_PAINTABLE (ptr: self)); |
364 | |
365 | g_object_unref (object: old_image); |
366 | |
367 | gdk_paintable_invalidate_contents (paintable: GDK_PAINTABLE (ptr: self)); |
368 | } |
369 | else |
370 | { |
371 | g_clear_object (&self->pending_image); |
372 | self->pending_update_cb = 0; |
373 | } |
374 | |
375 | return G_SOURCE_REMOVE; |
376 | } |
377 | |
378 | void |
379 | gtk_widget_paintable_update_image (GtkWidgetPaintable *self) |
380 | { |
381 | GdkPaintable *pending_image; |
382 | |
383 | if (self->pending_update_cb == 0) |
384 | { |
385 | self->pending_update_cb = g_idle_add_full (G_PRIORITY_HIGH, |
386 | function: gtk_widget_paintable_update_func, |
387 | data: self, |
388 | NULL); |
389 | } |
390 | |
391 | pending_image = gtk_widget_paintable_snapshot_widget (self); |
392 | g_set_object (&self->pending_image, pending_image); |
393 | g_object_unref (object: pending_image); |
394 | } |
395 | |
396 | void |
397 | gtk_widget_paintable_push_snapshot_count (GtkWidgetPaintable *self) |
398 | { |
399 | self->snapshot_count++; |
400 | } |
401 | |
402 | void |
403 | gtk_widget_paintable_pop_snapshot_count (GtkWidgetPaintable *self) |
404 | { |
405 | self->snapshot_count--; |
406 | } |
407 | |