GCC Code Coverage Report


Directory: ./
File: panels/system/users/cc-crop-area.c
Date: 2024-05-04 07:58:27
Exec Total Coverage
Lines: 0 334 0.0%
Functions: 0 23 0.0%
Branches: 0 84 0.0%

Line Branch Exec Source
1 /*
2 * Copyright 2021 Red Hat, Inc,
3 *
4 * Authors:
5 * - Matthias Clasen <mclasen@redhat.com>
6 * - Niels De Graef <nielsdg@redhat.com>
7 *
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with this program; if not, see <http://www.gnu.org/licenses/>.
20 */
21
22 #include "config.h"
23
24 #include <glib.h>
25 #include <glib/gi18n.h>
26 #include <gtk/gtk.h>
27 #include <gsk/gl/gskglrenderer.h>
28
29 #include "cc-crop-area.h"
30
31 /**
32 * CcCropArea:
33 *
34 * A widget that shows a [iface@Gdk.Paintable] and allows the user specify a
35 * cropping rectangle to effectively crop to that given area.
36 */
37
38 /* Location of the cursor relative to the cropping rectangle/circle */
39 typedef enum {
40 OUTSIDE,
41 INSIDE,
42 TOP,
43 TOP_LEFT,
44 TOP_RIGHT,
45 BOTTOM,
46 BOTTOM_LEFT,
47 BOTTOM_RIGHT,
48 LEFT,
49 RIGHT
50 } Location;
51
52 struct _CcCropArea {
53 GtkWidget parent_instance;
54
55 GdkPaintable *paintable;
56
57 double scale; /* scale factor to go from paintable size to widget size */
58
59 const char *current_cursor;
60 Location active_region;
61 double drag_offx;
62 double drag_offy;
63
64 /* In source coordinates. See get_scaled_crop() for widget coordinates */
65 GdkRectangle crop;
66
67 /* In widget coordinates */
68 GdkRectangle image;
69 int min_crop_width;
70 int min_crop_height;
71 };
72
73 G_DEFINE_TYPE (CcCropArea, cc_crop_area, GTK_TYPE_WIDGET);
74
75 static void
76 update_image_and_crop (CcCropArea *area)
77 {
78 GtkAllocation allocation;
79 int width, height;
80 int dest_width, dest_height;
81 double scale;
82
83 if (area->paintable == NULL)
84 return;
85
86 gtk_widget_get_allocation (GTK_WIDGET (area), &allocation);
87
88 /* Get the size of the paintable */
89 width = gdk_paintable_get_intrinsic_width (area->paintable);
90 height = gdk_paintable_get_intrinsic_height (area->paintable);
91
92 /* Find out the scale to convert to widget width/height */
93 scale = allocation.height / (double) height;
94 if (scale * width > allocation.width)
95 scale = allocation.width / (double) width;
96
97 dest_width = width * scale;
98 dest_height = height * scale;
99
100 if (area->scale == 0.0) {
101 double scale_to_80, scale_to_image, crop_scale;
102
103 /* Start with a crop area of 80% of the area, unless it's larger than min_size */
104 scale_to_80 = MIN ((double) dest_width * 0.8, (double) dest_height * 0.8);
105 scale_to_image = MIN ((double) area->min_crop_width, (double) area->min_crop_height);
106 crop_scale = MAX (scale_to_80, scale_to_image);
107
108 /* Divide by `scale` to get back to paintable coordinates */
109 area->crop.width = crop_scale / scale;
110 area->crop.height = crop_scale / scale;
111 area->crop.x = (width - area->crop.width) / 2;
112 area->crop.y = (height - area->crop.height) / 2;
113 }
114
115 area->scale = scale;
116 area->image.x = (allocation.width - dest_width) / 2;
117 area->image.y = (allocation.height - dest_height) / 2;
118 area->image.width = dest_width;
119 area->image.height = dest_height;
120 }
121
122 /* Returns area->crop in widget coordinates (vs paintable coordsinates) */
123 static void
124 get_scaled_crop (CcCropArea *area,
125 GdkRectangle *crop)
126 {
127 crop->x = area->image.x + area->crop.x * area->scale;
128 crop->y = area->image.y + area->crop.y * area->scale;
129 crop->width = area->image.x + (area->crop.x + area->crop.width) * area->scale - crop->x;
130 crop->height = area->image.y + (area->crop.y + area->crop.height) * area->scale - crop->y;
131 }
132
133 typedef enum {
134 BELOW,
135 LOWER,
136 BETWEEN,
137 UPPER,
138 ABOVE
139 } Range;
140
141 static Range
142 find_range (int x,
143 int min,
144 int max)
145 {
146 int tolerance = 12;
147
148 if (x < min - tolerance)
149 return BELOW;
150 if (x <= min + tolerance)
151 return LOWER;
152 if (x < max - tolerance)
153 return BETWEEN;
154 if (x <= max + tolerance)
155 return UPPER;
156 return ABOVE;
157 }
158
159 /* Finds the location of (@x, @y) relative to the crop @rect */
160 static Location
161 find_location (GdkRectangle *rect,
162 int x,
163 int y)
164 {
165 Range x_range, y_range;
166 Location location[5][5] = {
167 { OUTSIDE, OUTSIDE, OUTSIDE, OUTSIDE, OUTSIDE },
168 { OUTSIDE, TOP_LEFT, TOP, TOP_RIGHT, OUTSIDE },
169 { OUTSIDE, LEFT, INSIDE, RIGHT, OUTSIDE },
170 { OUTSIDE, BOTTOM_LEFT, BOTTOM, BOTTOM_RIGHT, OUTSIDE },
171 { OUTSIDE, OUTSIDE, OUTSIDE, OUTSIDE, OUTSIDE }
172 };
173
174 x_range = find_range (x, rect->x, rect->x + rect->width);
175 y_range = find_range (y, rect->y, rect->y + rect->height);
176
177 return location[y_range][x_range];
178 }
179
180 static void
181 update_cursor (CcCropArea *area,
182 int x,
183 int y)
184 {
185 const char *cursor_type;
186 GdkRectangle crop;
187 int region;
188
189 region = area->active_region;
190 if (region == OUTSIDE) {
191 get_scaled_crop (area, &crop);
192 region = find_location (&crop, x, y);
193 }
194
195 switch (region) {
196 case OUTSIDE:
197 cursor_type = "default";
198 break;
199 case TOP_LEFT:
200 cursor_type = "nw-resize";
201 break;
202 case TOP:
203 cursor_type = "n-resize";
204 break;
205 case TOP_RIGHT:
206 cursor_type = "ne-resize";
207 break;
208 case LEFT:
209 cursor_type = "w-resize";
210 break;
211 case INSIDE:
212 cursor_type = "move";
213 break;
214 case RIGHT:
215 cursor_type = "e-resize";
216 break;
217 case BOTTOM_LEFT:
218 cursor_type = "sw-resize";
219 break;
220 case BOTTOM:
221 cursor_type = "s-resize";
222 break;
223 case BOTTOM_RIGHT:
224 cursor_type = "se-resize";
225 break;
226 default:
227 g_assert_not_reached ();
228 }
229
230 if (cursor_type != area->current_cursor) {
231 GtkNative *native;
232 g_autoptr (GdkCursor) cursor = NULL;
233
234 native = gtk_widget_get_native (GTK_WIDGET (area));
235 if (!native) {
236 g_warning ("Can't adjust cursor: no GtkNative found");
237 return;
238 }
239 cursor = gdk_cursor_new_from_name (cursor_type, NULL);
240 gdk_surface_set_cursor (gtk_native_get_surface (native), cursor);
241 area->current_cursor = cursor_type;
242 }
243 }
244
245 static gboolean
246 on_motion (CcCropArea *area,
247 double event_x,
248 double event_y)
249 {
250 if (area->paintable == NULL)
251 return FALSE;
252
253 update_cursor (area, event_x, event_y);
254
255 return FALSE;
256 }
257
258 static void
259 on_leave (CcCropArea *area)
260 {
261 if (area->paintable == NULL)
262 return;
263
264 /* Restore 'default' cursor */
265 update_cursor (area, 0, 0);
266 }
267
268 static void
269 on_drag_begin (CcCropArea *area,
270 double start_x,
271 double start_y)
272 {
273 GdkRectangle crop;
274
275 if (area->paintable == NULL)
276 return;
277
278 update_cursor (area, start_x, start_y);
279
280 get_scaled_crop (area, &crop);
281
282 area->active_region = find_location (&crop, start_x, start_y);
283
284 area->drag_offx = 0.0;
285 area->drag_offy = 0.0;
286 }
287
288 static void
289 on_drag_update (CcCropArea *area,
290 double offset_x,
291 double offset_y,
292 GtkGestureDrag *gesture)
293 {
294 double start_x, start_y;
295 int x, y, delta_x, delta_y;
296 int clamped_delta_x, clamped_delta_y;
297 int left, right, top, bottom;
298 int center_x, center_y;
299 int distance_left, distance_right, distance_top, distance_bottom;
300 int closest_distance_x, closest_distance_y;
301 int size_x, size_y;
302 int min_size, max_size, wanted_size, new_size;
303
304 gtk_gesture_drag_get_start_point (gesture, &start_x, &start_y);
305
306 /* Get the x, y, dx, dy in paintable coords */
307 x = (start_x + offset_x - area->image.x) / area->scale;
308 y = (start_y + offset_y - area->image.y) / area->scale;
309 delta_x = (offset_x - area->drag_offx) / area->scale;
310 delta_y = (offset_y - area->drag_offy) / area->scale;
311
312 /* Helper variables */
313 left = area->crop.x;
314 right = area->crop.x + area->crop.width - 1;
315 top = area->crop.y;
316 bottom = area->crop.y + area->crop.height - 1;
317
318 center_x = (left + right) / 2;
319 center_y = (top + bottom) / 2;
320
321 distance_left = left;
322 distance_right = gdk_paintable_get_intrinsic_width (area->paintable) - (right + 1);
323 distance_top = top;
324 distance_bottom = gdk_paintable_get_intrinsic_height (area->paintable) - (bottom + 1);
325
326 closest_distance_x = MIN (distance_left, distance_right);
327 closest_distance_y = MIN (distance_top, distance_bottom);
328
329 /* All size variables are center-to-center, not edge-to-edge, hence the missing '+ 1' everywhere */
330 size_x = right - left;
331 size_y = bottom - top;
332
333 min_size = MAX (area->min_crop_width / area->scale, area->min_crop_height / area->scale);
334
335 /* What we have to do depends on where the user started dragging */
336 switch (area->active_region) {
337 case INSIDE:
338 if (delta_x < 0)
339 clamped_delta_x = MAX (delta_x, -distance_left);
340 else
341 clamped_delta_x = MIN (delta_x, distance_right);
342
343 if (delta_y < 0)
344 clamped_delta_y = MAX (delta_y, -distance_top);
345 else
346 clamped_delta_y = MIN (delta_y, distance_bottom);
347
348 left += clamped_delta_x;
349 right += clamped_delta_x;
350 top += clamped_delta_y;
351 bottom += clamped_delta_y;
352
353 break;
354
355 /* The wanted size assumes one side remains glued to the cursor */
356 case TOP_LEFT:
357 max_size = MIN (size_y + distance_top, size_x + distance_left);
358 wanted_size = MAX (bottom - y, right - x);
359 new_size = CLAMP (wanted_size, MIN (min_size, max_size), max_size);
360 top = bottom - new_size;
361 left = right - new_size;
362 break;
363
364 case TOP:
365 max_size = MIN (size_y + distance_top, size_x + 2 * closest_distance_x);
366 wanted_size = bottom - y;
367 new_size = CLAMP (wanted_size, MIN (min_size, max_size), max_size);
368 top = bottom - new_size;
369 left = center_x - new_size / 2;
370 right = left + new_size;
371 break;
372
373 case TOP_RIGHT:
374 max_size = MIN (size_y + distance_top, size_x + distance_right);
375 wanted_size = MAX (bottom - y, x - left);
376 new_size = CLAMP (wanted_size, MIN (min_size, max_size), max_size);
377 top = bottom - new_size;
378 right = left + new_size;
379 break;
380
381 case LEFT:
382 max_size = MIN (size_x + distance_left, size_y + 2 * closest_distance_y);
383 wanted_size = right - x;
384 new_size = CLAMP (wanted_size, MIN (min_size, max_size), max_size);
385 left = right - new_size;
386 top = center_y - new_size / 2;
387 bottom = top + new_size;
388 break;
389
390 case BOTTOM_LEFT:
391 max_size = MIN (size_y + distance_bottom, size_x + distance_left);
392 wanted_size = MAX (y - top, right - x);
393 new_size = CLAMP (wanted_size, MIN (min_size, max_size), max_size);
394 bottom = top + new_size;
395 left = right - new_size;
396 break;
397
398 case RIGHT:
399 max_size = MIN (size_x + distance_right, size_y + 2 * closest_distance_y);
400 wanted_size = x - left;
401 new_size = CLAMP (wanted_size, MIN (min_size, max_size), max_size);
402 right = left + new_size;
403 top = center_y - new_size / 2;
404 bottom = top + new_size;
405 break;
406
407 case BOTTOM_RIGHT:
408 max_size = MIN (size_y + distance_bottom, size_x + distance_right);
409 wanted_size = MAX (y - top, x - left);
410 new_size = CLAMP (wanted_size, MIN (min_size, max_size), max_size);
411 bottom = top + new_size;
412 right = left + new_size;
413 break;
414
415 case BOTTOM:
416 max_size = MIN (size_y + distance_bottom, size_x + 2 * closest_distance_x);
417 wanted_size = y - top;
418 new_size = CLAMP (wanted_size, MIN (min_size, max_size), max_size);
419 bottom = top + new_size;
420 left = center_x - new_size / 2;
421 right = left + new_size;
422 break;
423
424 default:
425 return;
426 }
427
428 area->crop.x = left;
429 area->crop.y = top;
430 area->crop.width = right - left + 1;
431 area->crop.height = bottom - top + 1;
432
433 /* Only update drag_off based on the rounded deltas, otherwise rounding accumulates */
434 area->drag_offx += area->scale * delta_x;
435 area->drag_offy += area->scale * delta_y;
436
437 gtk_widget_queue_draw (GTK_WIDGET (area));
438 }
439
440 static void
441 on_drag_end (CcCropArea *area,
442 double offset_x,
443 double offset_y)
444 {
445 area->active_region = OUTSIDE;
446 area->drag_offx = 0.0;
447 area->drag_offy = 0.0;
448 }
449
450 static void
451 on_drag_cancel (CcCropArea *area,
452 GdkEventSequence *sequence)
453 {
454 area->active_region = OUTSIDE;
455 area->drag_offx = 0;
456 area->drag_offy = 0;
457 }
458
459 #define CORNER_LINE_WIDTH 4.0
460 #define CORNER_LINE_LENGTH 15.0
461 #define CORNER_SIZE (CORNER_LINE_LENGTH + CORNER_LINE_WIDTH / 2)
462
463 static void
464 cc_crop_area_snapshot (GtkWidget *widget,
465 GtkSnapshot *snapshot)
466 {
467 CcCropArea *area = CC_CROP_AREA (widget);
468 cairo_t *cr;
469 GdkRectangle crop;
470
471 if (area->paintable == NULL)
472 return;
473
474 update_image_and_crop (area);
475
476
477 gtk_snapshot_save (snapshot);
478
479 /* First draw the picture */
480 gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (area->image.x, area->image.y));
481
482 gdk_paintable_snapshot (area->paintable, snapshot, area->image.width, area->image.height);
483
484 /* Draw the cropping UI on top with cairo */
485 cr = gtk_snapshot_append_cairo (snapshot, &GRAPHENE_RECT_INIT (0, 0, area->image.width, area->image.height));
486
487 get_scaled_crop (area, &crop);
488 crop.x -= area->image.x;
489 crop.y -= area->image.y;
490
491 /* Draw the circle as an ellipse, to prevent rounding from jitter of the edges */
492 cairo_save (cr);
493 cairo_translate (cr, crop.x + crop.width / 2.0, crop.y + crop.height / 2.0);
494 cairo_scale (cr, crop.width / 2.0, crop.height / 2.0);
495 cairo_arc (cr, 0, 0, 1, 0, 2 * G_PI);
496 cairo_restore (cr);
497 cairo_save (cr);
498 cairo_rectangle (cr, 0, 0, area->image.width, area->image.height);
499 cairo_set_source_rgba (cr, 0, 0, 0, 0.4);
500 cairo_set_fill_rule (cr, CAIRO_FILL_RULE_EVEN_ODD);
501 cairo_fill (cr);
502 cairo_restore (cr);
503
504 /* draw the four corners */
505 cairo_set_source_rgb (cr, 1, 1, 1);
506 cairo_set_line_width (cr, CORNER_LINE_WIDTH);
507
508 /* top left corner */
509 cairo_move_to (cr, crop.x + CORNER_LINE_WIDTH / 2, crop.y + CORNER_SIZE);
510 cairo_rel_line_to (cr, 0, -CORNER_LINE_LENGTH);
511 cairo_rel_line_to (cr, CORNER_LINE_LENGTH, 0);
512 /* top right corner */
513 cairo_rel_move_to (cr, crop.width - 2 * CORNER_SIZE, 0);
514 cairo_rel_line_to (cr, CORNER_LINE_LENGTH, 0);
515 cairo_rel_line_to (cr, 0, CORNER_LINE_LENGTH);
516 /* bottom right corner */
517 cairo_rel_move_to (cr, 0, crop.height - 2 * CORNER_SIZE);
518 cairo_rel_line_to (cr, 0, CORNER_LINE_LENGTH);
519 cairo_rel_line_to (cr, -CORNER_LINE_LENGTH, 0);
520 /* bottom left corner */
521 cairo_rel_move_to (cr, -(crop.width - 2 * CORNER_SIZE), 0);
522 cairo_rel_line_to (cr, -CORNER_LINE_LENGTH, 0);
523 cairo_rel_line_to (cr, 0, -CORNER_LINE_LENGTH);
524
525 cairo_stroke (cr);
526
527 gtk_snapshot_restore (snapshot);
528 }
529
530 static void
531 cc_crop_area_finalize (GObject *object)
532 {
533 CcCropArea *area = CC_CROP_AREA (object);
534
535 g_clear_object (&area->paintable);
536 }
537
538 static void
539 cc_crop_area_class_init (CcCropAreaClass *klass)
540 {
541 GObjectClass *object_class = G_OBJECT_CLASS (klass);
542 GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
543
544 object_class->finalize = cc_crop_area_finalize;
545
546 widget_class->snapshot = cc_crop_area_snapshot;
547 }
548
549 static void
550 cc_crop_area_init (CcCropArea *area)
551 {
552 GtkGesture *gesture;
553 GtkEventController *controller;
554
555 /* Add handlers for dragging */
556 gesture = gtk_gesture_drag_new ();
557 g_signal_connect_swapped (gesture, "drag-begin", G_CALLBACK (on_drag_begin), area);
558 g_signal_connect_swapped (gesture, "drag-update", G_CALLBACK (on_drag_update), area);
559 g_signal_connect_swapped (gesture, "drag-end", G_CALLBACK (on_drag_end), area);
560 g_signal_connect_swapped (gesture, "cancel", G_CALLBACK (on_drag_cancel), area);
561 gtk_widget_add_controller (GTK_WIDGET (area), GTK_EVENT_CONTROLLER (gesture));
562
563 /* Add handlers for motion events */
564 controller = gtk_event_controller_motion_new ();
565 g_signal_connect_swapped (controller, "motion", G_CALLBACK (on_motion), area);
566 g_signal_connect_swapped (controller, "leave", G_CALLBACK (on_leave), area);
567 gtk_widget_add_controller (GTK_WIDGET (area), GTK_EVENT_CONTROLLER (controller));
568
569 area->scale = 0.0;
570 area->image.x = 0;
571 area->image.y = 0;
572 area->image.width = 0;
573 area->image.height = 0;
574 area->active_region = OUTSIDE;
575 area->min_crop_width = 48;
576 area->min_crop_height = 48;
577
578 gtk_widget_set_size_request (GTK_WIDGET (area), 48, 48);
579 }
580
581 GtkWidget *
582 cc_crop_area_new (void)
583 {
584 return g_object_new (CC_TYPE_CROP_AREA, NULL);
585 }
586
587 /**
588 * cc_crop_area_create_pixbuf:
589 * @area: A crop area
590 *
591 * Renders the area's paintable, with the cropping applied by the user, into a
592 * GdkPixbuf.
593 *
594 * Returns: (transfer full): The cropped picture
595 */
596 GdkPixbuf *
597 cc_crop_area_create_pixbuf (CcCropArea *area)
598 {
599 g_autoptr (GtkSnapshot) snapshot = NULL;
600 g_autoptr (GskRenderNode) node = NULL;
601 g_autoptr (GskRenderer) renderer = NULL;
602 g_autoptr (GdkTexture) texture = NULL;
603 g_autoptr (GError) error = NULL;
604 graphene_rect_t viewport;
605
606 g_return_val_if_fail (CC_IS_CROP_AREA (area), NULL);
607
608 snapshot = gtk_snapshot_new ();
609 gdk_paintable_snapshot (area->paintable, snapshot,
610 gdk_paintable_get_intrinsic_width (area->paintable),
611 gdk_paintable_get_intrinsic_height (area->paintable));
612 node = gtk_snapshot_free_to_node (g_steal_pointer (&snapshot));
613
614 renderer = gsk_gl_renderer_new ();
615 if (!gsk_renderer_realize (renderer, NULL, &error)) {
616 g_warning ("Couldn't realize GL renderer: %s", error->message);
617 return NULL;
618 }
619 viewport = GRAPHENE_RECT_INIT (area->crop.x, area->crop.y,
620 area->crop.width, area->crop.height);
621 texture = gsk_renderer_render_texture (renderer, node, &viewport);
622 gsk_renderer_unrealize (renderer);
623
624 return gdk_pixbuf_get_from_texture (texture);
625 }
626
627 /**
628 * cc_crop_area_get_paintable:
629 * @area: A crop area
630 *
631 * Returns the area's paintable, unmodified.
632 *
633 * Returns: (transfer none) (nullable): The paintable which the user can crop
634 */
635 GdkPaintable *
636 cc_crop_area_get_paintable (CcCropArea *area)
637 {
638 g_return_val_if_fail (CC_IS_CROP_AREA (area), NULL);
639
640 return area->paintable;
641 }
642
643 void
644 cc_crop_area_set_paintable (CcCropArea *area,
645 GdkPaintable *paintable)
646 {
647 g_return_if_fail (CC_IS_CROP_AREA (area));
648 g_return_if_fail (GDK_IS_PAINTABLE (paintable));
649
650 g_set_object (&area->paintable, paintable);
651
652 area->scale = 0.0;
653 area->image.x = 0;
654 area->image.y = 0;
655 area->image.width = 0;
656 area->image.height = 0;
657
658 gtk_widget_queue_draw (GTK_WIDGET (area));
659 }
660
661 /**
662 * cc_crop_area_set_min_size:
663 * @area: A crop widget
664 * @width: The minimal width
665 * @height: The minimal height
666 *
667 * Sets the minimal size of the crop rectangle (in paintable coordinates)
668 */
669 void
670 cc_crop_area_set_min_size (CcCropArea *area,
671 int width,
672 int height)
673 {
674 g_return_if_fail (CC_IS_CROP_AREA (area));
675
676 area->min_crop_width = width;
677 area->min_crop_height = height;
678
679 gtk_widget_set_size_request (GTK_WIDGET (area),
680 area->min_crop_width,
681 area->min_crop_height);
682 }
683