1 | /* Paint |
2 | * #Keywords: GdkDrawingArea, GtkGesture |
3 | * |
4 | * Demonstrates practical handling of drawing tablets in a real world |
5 | * usecase. |
6 | */ |
7 | #include <glib/gi18n.h> |
8 | #include <gtk/gtk.h> |
9 | |
10 | enum { |
11 | COLOR_SET, |
12 | N_SIGNALS |
13 | }; |
14 | |
15 | static guint area_signals[N_SIGNALS] = { 0, }; |
16 | |
17 | typedef struct |
18 | { |
19 | GtkWidget parent_instance; |
20 | cairo_surface_t *surface; |
21 | cairo_t *cr; |
22 | GdkRGBA draw_color; |
23 | GtkPadController *pad_controller; |
24 | double brush_size; |
25 | } DrawingArea; |
26 | |
27 | typedef struct |
28 | { |
29 | GtkWidgetClass parent_class; |
30 | } DrawingAreaClass; |
31 | |
32 | static GtkPadActionEntry pad_actions[] = { |
33 | { GTK_PAD_ACTION_BUTTON, 1, -1, N_("Black" ), "pad.black" }, |
34 | { GTK_PAD_ACTION_BUTTON, 2, -1, N_("Pink" ), "pad.pink" }, |
35 | { GTK_PAD_ACTION_BUTTON, 3, -1, N_("Green" ), "pad.green" }, |
36 | { GTK_PAD_ACTION_BUTTON, 4, -1, N_("Red" ), "pad.red" }, |
37 | { GTK_PAD_ACTION_BUTTON, 5, -1, N_("Purple" ), "pad.purple" }, |
38 | { GTK_PAD_ACTION_BUTTON, 6, -1, N_("Orange" ), "pad.orange" }, |
39 | { GTK_PAD_ACTION_STRIP, -1, -1, N_("Brush size" ), "pad.brush_size" }, |
40 | }; |
41 | |
42 | static const char *pad_colors[] = { |
43 | "black" , |
44 | "pink" , |
45 | "green" , |
46 | "red" , |
47 | "purple" , |
48 | "orange" |
49 | }; |
50 | |
51 | static GType drawing_area_get_type (void); |
52 | G_DEFINE_TYPE (DrawingArea, drawing_area, GTK_TYPE_WIDGET) |
53 | |
54 | static void drawing_area_set_color (DrawingArea *area, |
55 | GdkRGBA *color); |
56 | |
57 | static void |
58 | drawing_area_ensure_surface (DrawingArea *area, |
59 | int width, |
60 | int height) |
61 | { |
62 | if (!area->surface || |
63 | cairo_image_surface_get_width (surface: area->surface) != width || |
64 | cairo_image_surface_get_height (surface: area->surface) != height) |
65 | { |
66 | cairo_surface_t *surface; |
67 | |
68 | surface = cairo_image_surface_create (format: CAIRO_FORMAT_ARGB32, |
69 | width, height); |
70 | if (area->surface) |
71 | { |
72 | cairo_t *cr; |
73 | |
74 | cr = cairo_create (target: surface); |
75 | cairo_set_source_surface (cr, surface: area->surface, x: 0, y: 0); |
76 | cairo_paint (cr); |
77 | |
78 | cairo_surface_destroy (surface: area->surface); |
79 | cairo_destroy (cr: area->cr); |
80 | cairo_destroy (cr); |
81 | } |
82 | |
83 | area->surface = surface; |
84 | area->cr = cairo_create (target: surface); |
85 | } |
86 | } |
87 | |
88 | static void |
89 | drawing_area_size_allocate (GtkWidget *widget, |
90 | int width, |
91 | int height, |
92 | int baseline) |
93 | { |
94 | DrawingArea *area = (DrawingArea *) widget; |
95 | |
96 | drawing_area_ensure_surface (area, width, height); |
97 | |
98 | GTK_WIDGET_CLASS (drawing_area_parent_class)->size_allocate (widget, width, height, baseline); |
99 | } |
100 | |
101 | static void |
102 | drawing_area_map (GtkWidget *widget) |
103 | { |
104 | GtkAllocation allocation; |
105 | |
106 | GTK_WIDGET_CLASS (drawing_area_parent_class)->map (widget); |
107 | |
108 | gtk_widget_get_allocation (widget, allocation: &allocation); |
109 | drawing_area_ensure_surface (area: (DrawingArea *) widget, |
110 | width: allocation.width, height: allocation.height); |
111 | } |
112 | |
113 | static void |
114 | drawing_area_unmap (GtkWidget *widget) |
115 | { |
116 | DrawingArea *area = (DrawingArea *) widget; |
117 | |
118 | g_clear_pointer (&area->cr, cairo_destroy); |
119 | g_clear_pointer (&area->surface, cairo_surface_destroy); |
120 | |
121 | GTK_WIDGET_CLASS (drawing_area_parent_class)->unmap (widget); |
122 | } |
123 | |
124 | static void |
125 | drawing_area_snapshot (GtkWidget *widget, |
126 | GtkSnapshot *snapshot) |
127 | { |
128 | DrawingArea *area = (DrawingArea *) widget; |
129 | GtkAllocation allocation; |
130 | cairo_t *cr; |
131 | |
132 | gtk_widget_get_allocation (widget, allocation: &allocation); |
133 | cr = gtk_snapshot_append_cairo (snapshot, |
134 | bounds: &GRAPHENE_RECT_INIT ( |
135 | 0, 0, |
136 | allocation.width, |
137 | allocation.height |
138 | )); |
139 | |
140 | cairo_set_source_rgb (cr, red: 1, green: 1, blue: 1); |
141 | cairo_paint (cr); |
142 | |
143 | cairo_set_source_surface (cr, surface: area->surface, x: 0, y: 0); |
144 | cairo_paint (cr); |
145 | |
146 | cairo_set_source_rgb (cr, red: 0.6, green: 0.6, blue: 0.6); |
147 | cairo_rectangle (cr, x: 0, y: 0, width: allocation.width, height: allocation.height); |
148 | cairo_stroke (cr); |
149 | |
150 | cairo_destroy (cr); |
151 | } |
152 | |
153 | static void |
154 | on_pad_button_activate (GSimpleAction *action, |
155 | GVariant *parameter, |
156 | DrawingArea *area) |
157 | { |
158 | const char *color = g_object_get_data (G_OBJECT (action), key: "color" ); |
159 | GdkRGBA rgba; |
160 | |
161 | gdk_rgba_parse (rgba: &rgba, spec: color); |
162 | drawing_area_set_color (area, color: &rgba); |
163 | } |
164 | |
165 | static void |
166 | on_pad_knob_change (GSimpleAction *action, |
167 | GVariant *parameter, |
168 | DrawingArea *area) |
169 | { |
170 | double value = g_variant_get_double (value: parameter); |
171 | |
172 | area->brush_size = value; |
173 | } |
174 | |
175 | static void |
176 | drawing_area_unroot (GtkWidget *widget) |
177 | { |
178 | DrawingArea *area = (DrawingArea *) widget; |
179 | GtkWidget *toplevel; |
180 | |
181 | toplevel = GTK_WIDGET (gtk_widget_get_root (widget)); |
182 | |
183 | if (area->pad_controller) |
184 | { |
185 | gtk_widget_remove_controller (widget: toplevel, GTK_EVENT_CONTROLLER (area->pad_controller)); |
186 | area->pad_controller = NULL; |
187 | } |
188 | |
189 | GTK_WIDGET_CLASS (drawing_area_parent_class)->unroot (widget); |
190 | } |
191 | |
192 | static void |
193 | drawing_area_root (GtkWidget *widget) |
194 | { |
195 | DrawingArea *area = (DrawingArea *) widget; |
196 | GSimpleActionGroup *action_group; |
197 | GSimpleAction *action; |
198 | GtkWidget *toplevel; |
199 | int i; |
200 | |
201 | GTK_WIDGET_CLASS (drawing_area_parent_class)->root (widget); |
202 | |
203 | toplevel = GTK_WIDGET (gtk_widget_get_root (GTK_WIDGET (area))); |
204 | |
205 | action_group = g_simple_action_group_new (); |
206 | area->pad_controller = gtk_pad_controller_new (G_ACTION_GROUP (action_group), NULL); |
207 | |
208 | for (i = 0; i < G_N_ELEMENTS (pad_actions); i++) |
209 | { |
210 | if (pad_actions[i].type == GTK_PAD_ACTION_BUTTON) |
211 | { |
212 | action = g_simple_action_new (name: pad_actions[i].action_name, NULL); |
213 | g_object_set_data (G_OBJECT (action), key: "color" , |
214 | data: (gpointer) pad_colors[i]); |
215 | g_signal_connect (action, "activate" , |
216 | G_CALLBACK (on_pad_button_activate), area); |
217 | } |
218 | else |
219 | { |
220 | action = g_simple_action_new_stateful (name: pad_actions[i].action_name, |
221 | G_VARIANT_TYPE_DOUBLE, NULL); |
222 | g_signal_connect (action, "activate" , |
223 | G_CALLBACK (on_pad_knob_change), area); |
224 | } |
225 | |
226 | g_action_map_add_action (G_ACTION_MAP (action_group), G_ACTION (action)); |
227 | g_object_unref (object: action); |
228 | } |
229 | |
230 | gtk_pad_controller_set_action_entries (controller: area->pad_controller, entries: pad_actions, |
231 | G_N_ELEMENTS (pad_actions)); |
232 | |
233 | gtk_widget_add_controller (widget: toplevel, GTK_EVENT_CONTROLLER (area->pad_controller)); |
234 | } |
235 | |
236 | static void |
237 | drawing_area_class_init (DrawingAreaClass *klass) |
238 | { |
239 | GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); |
240 | |
241 | widget_class->size_allocate = drawing_area_size_allocate; |
242 | widget_class->snapshot = drawing_area_snapshot; |
243 | widget_class->map = drawing_area_map; |
244 | widget_class->unmap = drawing_area_unmap; |
245 | widget_class->root = drawing_area_root; |
246 | widget_class->unroot = drawing_area_unroot; |
247 | |
248 | area_signals[COLOR_SET] = |
249 | g_signal_new (signal_name: "color-set" , |
250 | G_TYPE_FROM_CLASS (widget_class), |
251 | signal_flags: G_SIGNAL_RUN_FIRST, |
252 | class_offset: 0, NULL, NULL, NULL, |
253 | G_TYPE_NONE, n_params: 1, GDK_TYPE_RGBA); |
254 | } |
255 | |
256 | static void |
257 | drawing_area_apply_stroke (DrawingArea *area, |
258 | GdkDeviceTool *tool, |
259 | double x, |
260 | double y, |
261 | double pressure) |
262 | { |
263 | if (gdk_device_tool_get_tool_type (tool) == GDK_DEVICE_TOOL_TYPE_ERASER) |
264 | { |
265 | cairo_set_line_width (cr: area->cr, width: 10 * pressure * area->brush_size); |
266 | cairo_set_operator (cr: area->cr, op: CAIRO_OPERATOR_DEST_OUT); |
267 | } |
268 | else |
269 | { |
270 | cairo_set_line_width (cr: area->cr, width: 4 * pressure * area->brush_size); |
271 | cairo_set_operator (cr: area->cr, op: CAIRO_OPERATOR_SATURATE); |
272 | } |
273 | |
274 | cairo_set_source_rgba (cr: area->cr, red: area->draw_color.red, |
275 | green: area->draw_color.green, blue: area->draw_color.blue, |
276 | alpha: area->draw_color.alpha * pressure); |
277 | |
278 | cairo_line_to (cr: area->cr, x, y); |
279 | cairo_stroke (cr: area->cr); |
280 | cairo_move_to (cr: area->cr, x, y); |
281 | } |
282 | |
283 | static void |
284 | stylus_gesture_down (GtkGestureStylus *gesture, |
285 | double x, |
286 | double y, |
287 | DrawingArea *area) |
288 | { |
289 | cairo_new_path (cr: area->cr); |
290 | } |
291 | |
292 | static void |
293 | stylus_gesture_motion (GtkGestureStylus *gesture, |
294 | double x, |
295 | double y, |
296 | DrawingArea *area) |
297 | { |
298 | GdkTimeCoord *backlog; |
299 | GdkDeviceTool *tool; |
300 | double pressure; |
301 | guint n_items; |
302 | |
303 | tool = gtk_gesture_stylus_get_device_tool (gesture); |
304 | |
305 | if (gtk_gesture_stylus_get_backlog (gesture, backlog: &backlog, n_elems: &n_items)) |
306 | { |
307 | guint i; |
308 | |
309 | for (i = 0; i < n_items; i++) |
310 | { |
311 | drawing_area_apply_stroke (area, tool, |
312 | x: backlog[i].axes[GDK_AXIS_X], |
313 | y: backlog[i].axes[GDK_AXIS_Y], |
314 | pressure: backlog[i].axes[GDK_AXIS_PRESSURE]); |
315 | } |
316 | |
317 | g_free (mem: backlog); |
318 | } |
319 | else |
320 | { |
321 | if (!gtk_gesture_stylus_get_axis (gesture, axis: GDK_AXIS_PRESSURE, value: &pressure)) |
322 | pressure = 1; |
323 | |
324 | drawing_area_apply_stroke (area, tool, x, y, pressure); |
325 | } |
326 | |
327 | gtk_widget_queue_draw (GTK_WIDGET (area)); |
328 | } |
329 | |
330 | static void |
331 | drawing_area_init (DrawingArea *area) |
332 | { |
333 | GtkGesture *gesture; |
334 | |
335 | gesture = gtk_gesture_stylus_new (); |
336 | g_signal_connect (gesture, "down" , |
337 | G_CALLBACK (stylus_gesture_down), area); |
338 | g_signal_connect (gesture, "motion" , |
339 | G_CALLBACK (stylus_gesture_motion), area); |
340 | gtk_widget_add_controller (GTK_WIDGET (area), GTK_EVENT_CONTROLLER (gesture)); |
341 | |
342 | area->draw_color = (GdkRGBA) { 0, 0, 0, 1 }; |
343 | area->brush_size = 1; |
344 | } |
345 | |
346 | static GtkWidget * |
347 | drawing_area_new (void) |
348 | { |
349 | return g_object_new (object_type: drawing_area_get_type (), NULL); |
350 | } |
351 | |
352 | static void |
353 | drawing_area_set_color (DrawingArea *area, |
354 | GdkRGBA *color) |
355 | { |
356 | if (gdk_rgba_equal (p1: &area->draw_color, p2: color)) |
357 | return; |
358 | |
359 | area->draw_color = *color; |
360 | g_signal_emit (instance: area, signal_id: area_signals[COLOR_SET], detail: 0, &area->draw_color); |
361 | } |
362 | |
363 | static void |
364 | color_button_color_set (GtkColorButton *button, |
365 | DrawingArea *draw_area) |
366 | { |
367 | GdkRGBA color; |
368 | |
369 | gtk_color_chooser_get_rgba (GTK_COLOR_CHOOSER (button), color: &color); |
370 | drawing_area_set_color (area: draw_area, color: &color); |
371 | } |
372 | |
373 | static void |
374 | drawing_area_color_set (DrawingArea *area, |
375 | GdkRGBA *color, |
376 | GtkColorButton *button) |
377 | { |
378 | gtk_color_chooser_set_rgba (GTK_COLOR_CHOOSER (button), color); |
379 | } |
380 | |
381 | GtkWidget * |
382 | do_paint (GtkWidget *toplevel) |
383 | { |
384 | static GtkWidget *window = NULL; |
385 | |
386 | if (!window) |
387 | { |
388 | GtkWidget *draw_area, *, *colorbutton; |
389 | |
390 | window = gtk_window_new (); |
391 | |
392 | draw_area = drawing_area_new (); |
393 | gtk_window_set_child (GTK_WINDOW (window), child: draw_area); |
394 | |
395 | headerbar = gtk_header_bar_new (); |
396 | |
397 | colorbutton = gtk_color_button_new (); |
398 | g_signal_connect (colorbutton, "color-set" , |
399 | G_CALLBACK (color_button_color_set), draw_area); |
400 | g_signal_connect (draw_area, "color-set" , |
401 | G_CALLBACK (drawing_area_color_set), colorbutton); |
402 | gtk_color_chooser_set_rgba (GTK_COLOR_CHOOSER (colorbutton), |
403 | color: &(GdkRGBA) { 0, 0, 0, 1 }); |
404 | |
405 | gtk_header_bar_pack_end (GTK_HEADER_BAR (headerbar), child: colorbutton); |
406 | gtk_window_set_titlebar (GTK_WINDOW (window), titlebar: headerbar); |
407 | gtk_window_set_title (GTK_WINDOW (window), title: "Paint" ); |
408 | g_object_add_weak_pointer (G_OBJECT (window), weak_pointer_location: (gpointer *)&window); |
409 | } |
410 | |
411 | if (!gtk_widget_get_visible (widget: window)) |
412 | gtk_widget_show (widget: window); |
413 | else |
414 | gtk_window_destroy (GTK_WINDOW (window)); |
415 | |
416 | return window; |
417 | } |
418 | |