2022-01-26 10:13:58 +01:00
|
|
|
/*
|
|
|
|
* Copyright 2021 Red Hat, Inc,
|
2010-10-30 16:14:30 -04:00
|
|
|
*
|
2022-01-26 10:13:58 +01:00
|
|
|
* Authors:
|
|
|
|
* - Matthias Clasen <mclasen@redhat.com>
|
|
|
|
* - Niels De Graef <nielsdg@redhat.com>
|
2010-10-30 16:14:30 -04:00
|
|
|
*
|
|
|
|
* This program is free software; you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
2012-09-06 07:43:05 -04:00
|
|
|
* the Free Software Foundation; either version 2 of the License, or
|
2010-10-30 16:14:30 -04:00
|
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
2014-01-23 12:57:27 +01:00
|
|
|
* along with this program; if not, see <http://www.gnu.org/licenses/>.
|
2010-10-30 16:14:30 -04:00
|
|
|
*/
|
|
|
|
|
|
|
|
#include "config.h"
|
|
|
|
|
|
|
|
#include <glib.h>
|
|
|
|
#include <glib/gi18n.h>
|
|
|
|
#include <gtk/gtk.h>
|
2022-01-26 10:13:58 +01:00
|
|
|
#include <gsk/gl/gskglrenderer.h>
|
2010-10-30 16:14:30 -04:00
|
|
|
|
2014-10-15 13:40:08 +01:00
|
|
|
#include "cc-crop-area.h"
|
2010-10-30 16:14:30 -04:00
|
|
|
|
2022-01-26 10:13:58 +01:00
|
|
|
/**
|
|
|
|
* CcCropArea:
|
|
|
|
*
|
|
|
|
* A widget that shows a [iface@Gdk.Paintable] and allows the user specify a
|
|
|
|
* cropping rectangle to effectively crop to that given area.
|
|
|
|
*/
|
|
|
|
|
|
|
|
/* Location of the cursor relative to the cropping rectangle/circle */
|
|
|
|
typedef enum {
|
|
|
|
OUTSIDE,
|
|
|
|
INSIDE,
|
|
|
|
TOP,
|
|
|
|
TOP_LEFT,
|
|
|
|
TOP_RIGHT,
|
|
|
|
BOTTOM,
|
|
|
|
BOTTOM_LEFT,
|
|
|
|
BOTTOM_RIGHT,
|
|
|
|
LEFT,
|
|
|
|
RIGHT
|
|
|
|
} Location;
|
|
|
|
|
2018-05-31 10:42:09 +12:00
|
|
|
struct _CcCropArea {
|
2022-01-26 10:13:58 +01:00
|
|
|
GtkWidget parent_instance;
|
2010-10-30 16:14:30 -04:00
|
|
|
|
2022-01-26 10:13:58 +01:00
|
|
|
GdkPaintable *paintable;
|
2010-10-30 16:14:30 -04:00
|
|
|
|
2022-01-26 10:13:58 +01:00
|
|
|
double scale; /* scale factor to go from paintable size to widget size */
|
2010-10-30 16:14:30 -04:00
|
|
|
|
2022-01-26 10:13:58 +01:00
|
|
|
const char *current_cursor;
|
|
|
|
Location active_region;
|
|
|
|
double drag_offx;
|
|
|
|
double drag_offy;
|
2010-10-30 16:14:30 -04:00
|
|
|
|
2022-01-26 10:13:58 +01:00
|
|
|
/* In source coordinates. See get_scaled_crop() for widget coordinates */
|
|
|
|
GdkRectangle crop;
|
|
|
|
|
|
|
|
/* In widget coordinates */
|
|
|
|
GdkRectangle image;
|
|
|
|
int min_crop_width;
|
|
|
|
int min_crop_height;
|
|
|
|
};
|
|
|
|
|
|
|
|
G_DEFINE_TYPE (CcCropArea, cc_crop_area, GTK_TYPE_WIDGET);
|
2010-10-30 16:14:30 -04:00
|
|
|
|
|
|
|
static void
|
2022-01-26 10:13:58 +01:00
|
|
|
update_image_and_crop (CcCropArea *area)
|
2010-10-30 16:14:30 -04:00
|
|
|
{
|
2022-01-26 10:13:58 +01:00
|
|
|
GtkAllocation allocation;
|
|
|
|
int width, height;
|
|
|
|
int dest_width, dest_height;
|
|
|
|
double scale;
|
2010-10-30 16:14:30 -04:00
|
|
|
|
2022-01-26 10:13:58 +01:00
|
|
|
if (area->paintable == NULL)
|
|
|
|
return;
|
2010-10-30 16:14:30 -04:00
|
|
|
|
2022-01-26 10:13:58 +01:00
|
|
|
gtk_widget_get_allocation (GTK_WIDGET (area), &allocation);
|
2010-10-30 16:14:30 -04:00
|
|
|
|
2022-01-26 10:13:58 +01:00
|
|
|
/* Get the size of the paintable */
|
|
|
|
width = gdk_paintable_get_intrinsic_width (area->paintable);
|
|
|
|
height = gdk_paintable_get_intrinsic_height (area->paintable);
|
2010-10-30 16:14:30 -04:00
|
|
|
|
2022-01-26 10:13:58 +01:00
|
|
|
/* Find out the scale to convert to widget width/height */
|
|
|
|
scale = allocation.height / (double) height;
|
|
|
|
if (scale * width > allocation.width)
|
|
|
|
scale = allocation.width / (double) width;
|
|
|
|
|
|
|
|
dest_width = width * scale;
|
|
|
|
dest_height = height * scale;
|
|
|
|
|
|
|
|
if (area->scale == 0.0) {
|
|
|
|
double scale_to_80, scale_to_image, crop_scale;
|
|
|
|
|
|
|
|
/* Start with a crop area of 80% of the area, unless it's larger than min_size */
|
|
|
|
scale_to_80 = MIN ((double) dest_width * 0.8, (double) dest_height * 0.8);
|
|
|
|
scale_to_image = MIN ((double) area->min_crop_width, (double) area->min_crop_height);
|
|
|
|
crop_scale = MAX (scale_to_80, scale_to_image);
|
|
|
|
|
|
|
|
/* Divide by `scale` to get back to paintable coordinates */
|
|
|
|
area->crop.width = crop_scale / scale;
|
|
|
|
area->crop.height = crop_scale / scale;
|
|
|
|
area->crop.x = (width - area->crop.width) / 2;
|
|
|
|
area->crop.y = (height - area->crop.height) / 2;
|
|
|
|
}
|
|
|
|
|
|
|
|
area->scale = scale;
|
|
|
|
area->image.x = (allocation.width - dest_width) / 2;
|
|
|
|
area->image.y = (allocation.height - dest_height) / 2;
|
|
|
|
area->image.width = dest_width;
|
|
|
|
area->image.height = dest_height;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Returns area->crop in widget coordinates (vs paintable coordsinates) */
|
|
|
|
static void
|
|
|
|
get_scaled_crop (CcCropArea *area,
|
|
|
|
GdkRectangle *crop)
|
|
|
|
{
|
|
|
|
crop->x = area->image.x + area->crop.x * area->scale;
|
|
|
|
crop->y = area->image.y + area->crop.y * area->scale;
|
users: Improve rounding of avatar crop edges
If crop.x changes due to a drag, crop.x + crop.width remains fixed, as
thet is the opposite edge. However, in the mapping of paintable to
widget coordinates, crop->x + crop->width can vary due to rounding
errors. This fixes that, so that crop->x + crop->width does not vary,
with the same fix in the y direction.
However, the edges of the circle can still remain jittery due to integer
rounding, which is not fixed by using 'width / 2.0' instead of 'width /
2', since the width and height might differ by a pixel. Instead, draw an
ellipse, which removes edge jitter completely.
Part-of: <https://gitlab.gnome.org/GNOME/gnome-control-center/-/merge_requests/1824>
2023-06-08 23:38:06 +02:00
|
|
|
crop->width = area->image.x + (area->crop.x + area->crop.width) * area->scale - crop->x;
|
|
|
|
crop->height = area->image.y + (area->crop.y + area->crop.height) * area->scale - crop->y;
|
2010-10-30 16:14:30 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
typedef enum {
|
2022-01-26 10:13:58 +01:00
|
|
|
BELOW,
|
|
|
|
LOWER,
|
|
|
|
BETWEEN,
|
|
|
|
UPPER,
|
|
|
|
ABOVE
|
2010-10-30 16:14:30 -04:00
|
|
|
} Range;
|
|
|
|
|
|
|
|
static Range
|
2022-01-26 10:13:58 +01:00
|
|
|
find_range (int x,
|
|
|
|
int min,
|
|
|
|
int max)
|
2010-10-30 16:14:30 -04:00
|
|
|
{
|
2022-01-26 10:13:58 +01:00
|
|
|
int tolerance = 12;
|
|
|
|
|
|
|
|
if (x < min - tolerance)
|
|
|
|
return BELOW;
|
|
|
|
if (x <= min + tolerance)
|
|
|
|
return LOWER;
|
|
|
|
if (x < max - tolerance)
|
|
|
|
return BETWEEN;
|
|
|
|
if (x <= max + tolerance)
|
|
|
|
return UPPER;
|
|
|
|
return ABOVE;
|
2010-10-30 16:14:30 -04:00
|
|
|
}
|
|
|
|
|
2022-01-26 10:13:58 +01:00
|
|
|
/* Finds the location of (@x, @y) relative to the crop @rect */
|
2010-10-30 16:14:30 -04:00
|
|
|
static Location
|
|
|
|
find_location (GdkRectangle *rect,
|
2022-01-26 10:13:58 +01:00
|
|
|
int x,
|
|
|
|
int y)
|
2010-10-30 16:14:30 -04:00
|
|
|
{
|
2022-01-26 10:13:58 +01:00
|
|
|
Range x_range, y_range;
|
|
|
|
Location location[5][5] = {
|
|
|
|
{ OUTSIDE, OUTSIDE, OUTSIDE, OUTSIDE, OUTSIDE },
|
|
|
|
{ OUTSIDE, TOP_LEFT, TOP, TOP_RIGHT, OUTSIDE },
|
|
|
|
{ OUTSIDE, LEFT, INSIDE, RIGHT, OUTSIDE },
|
|
|
|
{ OUTSIDE, BOTTOM_LEFT, BOTTOM, BOTTOM_RIGHT, OUTSIDE },
|
|
|
|
{ OUTSIDE, OUTSIDE, OUTSIDE, OUTSIDE, OUTSIDE }
|
|
|
|
};
|
|
|
|
|
|
|
|
x_range = find_range (x, rect->x, rect->x + rect->width);
|
|
|
|
y_range = find_range (y, rect->y, rect->y + rect->height);
|
|
|
|
|
|
|
|
return location[y_range][x_range];
|
2010-10-30 16:14:30 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2014-10-15 13:40:08 +01:00
|
|
|
update_cursor (CcCropArea *area,
|
2022-01-26 10:13:58 +01:00
|
|
|
int x,
|
|
|
|
int y)
|
2010-10-30 16:14:30 -04:00
|
|
|
{
|
2022-01-26 10:13:58 +01:00
|
|
|
const char *cursor_type;
|
|
|
|
GdkRectangle crop;
|
|
|
|
int region;
|
|
|
|
|
|
|
|
region = area->active_region;
|
|
|
|
if (region == OUTSIDE) {
|
|
|
|
get_scaled_crop (area, &crop);
|
|
|
|
region = find_location (&crop, x, y);
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (region) {
|
|
|
|
case OUTSIDE:
|
|
|
|
cursor_type = "default";
|
|
|
|
break;
|
|
|
|
case TOP_LEFT:
|
|
|
|
cursor_type = "nw-resize";
|
|
|
|
break;
|
|
|
|
case TOP:
|
|
|
|
cursor_type = "n-resize";
|
|
|
|
break;
|
|
|
|
case TOP_RIGHT:
|
|
|
|
cursor_type = "ne-resize";
|
|
|
|
break;
|
|
|
|
case LEFT:
|
|
|
|
cursor_type = "w-resize";
|
|
|
|
break;
|
|
|
|
case INSIDE:
|
|
|
|
cursor_type = "move";
|
|
|
|
break;
|
|
|
|
case RIGHT:
|
|
|
|
cursor_type = "e-resize";
|
|
|
|
break;
|
|
|
|
case BOTTOM_LEFT:
|
|
|
|
cursor_type = "sw-resize";
|
|
|
|
break;
|
|
|
|
case BOTTOM:
|
|
|
|
cursor_type = "s-resize";
|
|
|
|
break;
|
|
|
|
case BOTTOM_RIGHT:
|
|
|
|
cursor_type = "se-resize";
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
g_assert_not_reached ();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (cursor_type != area->current_cursor) {
|
|
|
|
GtkNative *native;
|
|
|
|
g_autoptr (GdkCursor) cursor = NULL;
|
|
|
|
|
|
|
|
native = gtk_widget_get_native (GTK_WIDGET (area));
|
|
|
|
if (!native) {
|
|
|
|
g_warning ("Can't adjust cursor: no GtkNative found");
|
|
|
|
return;
|
2010-10-30 16:14:30 -04:00
|
|
|
}
|
2022-01-26 10:13:58 +01:00
|
|
|
cursor = gdk_cursor_new_from_name (cursor_type, NULL);
|
|
|
|
gdk_surface_set_cursor (gtk_native_get_surface (native), cursor);
|
|
|
|
area->current_cursor = cursor_type;
|
|
|
|
}
|
2010-10-30 16:14:30 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
static gboolean
|
2023-05-31 10:36:27 +12:00
|
|
|
on_motion (CcCropArea *area,
|
|
|
|
double event_x,
|
|
|
|
double event_y)
|
2010-10-30 16:14:30 -04:00
|
|
|
{
|
2022-01-26 10:13:58 +01:00
|
|
|
if (area->paintable == NULL)
|
|
|
|
return FALSE;
|
2010-10-30 16:14:30 -04:00
|
|
|
|
2022-01-26 10:13:58 +01:00
|
|
|
update_cursor (area, event_x, event_y);
|
2010-10-30 16:14:30 -04:00
|
|
|
|
2022-01-26 10:13:58 +01:00
|
|
|
return FALSE;
|
|
|
|
}
|
2010-10-30 16:14:30 -04:00
|
|
|
|
2023-02-18 21:48:09 -04:00
|
|
|
static void
|
2023-05-31 10:36:27 +12:00
|
|
|
on_leave (CcCropArea *area)
|
2023-02-18 21:48:09 -04:00
|
|
|
{
|
|
|
|
if (area->paintable == NULL)
|
|
|
|
return;
|
|
|
|
|
|
|
|
/* Restore 'default' cursor */
|
|
|
|
update_cursor (area, 0, 0);
|
|
|
|
}
|
|
|
|
|
2022-01-26 10:13:58 +01:00
|
|
|
static void
|
2023-05-31 10:36:27 +12:00
|
|
|
on_drag_begin (CcCropArea *area,
|
2022-01-26 10:13:58 +01:00
|
|
|
double start_x,
|
2023-05-31 10:36:27 +12:00
|
|
|
double start_y)
|
2022-01-26 10:13:58 +01:00
|
|
|
{
|
|
|
|
GdkRectangle crop;
|
2010-10-30 16:14:30 -04:00
|
|
|
|
2022-01-26 10:13:58 +01:00
|
|
|
if (area->paintable == NULL)
|
|
|
|
return;
|
2010-10-30 16:14:30 -04:00
|
|
|
|
2022-01-26 10:13:58 +01:00
|
|
|
update_cursor (area, start_x, start_y);
|
2010-10-30 16:14:30 -04:00
|
|
|
|
2022-01-26 10:13:58 +01:00
|
|
|
get_scaled_crop (area, &crop);
|
|
|
|
|
|
|
|
area->active_region = find_location (&crop, start_x, start_y);
|
|
|
|
|
|
|
|
area->drag_offx = 0.0;
|
|
|
|
area->drag_offy = 0.0;
|
2010-10-30 16:14:30 -04:00
|
|
|
}
|
|
|
|
|
2022-01-26 10:13:58 +01:00
|
|
|
static void
|
2023-05-31 10:36:27 +12:00
|
|
|
on_drag_update (CcCropArea *area,
|
2022-01-26 10:13:58 +01:00
|
|
|
double offset_x,
|
|
|
|
double offset_y,
|
2023-05-31 10:36:27 +12:00
|
|
|
GtkGestureDrag *gesture)
|
2010-10-30 16:14:30 -04:00
|
|
|
{
|
2022-01-26 10:13:58 +01:00
|
|
|
double start_x, start_y;
|
|
|
|
int x, y, delta_x, delta_y;
|
2023-06-09 00:48:05 +02:00
|
|
|
int clamped_delta_x, clamped_delta_y;
|
2022-01-26 10:13:58 +01:00
|
|
|
int left, right, top, bottom;
|
2023-06-09 00:48:05 +02:00
|
|
|
int center_x, center_y;
|
|
|
|
int distance_left, distance_right, distance_top, distance_bottom;
|
|
|
|
int closest_distance_x, closest_distance_y;
|
|
|
|
int size_x, size_y;
|
|
|
|
int min_size, max_size, wanted_size, new_size;
|
2022-01-26 10:13:58 +01:00
|
|
|
|
|
|
|
gtk_gesture_drag_get_start_point (gesture, &start_x, &start_y);
|
|
|
|
|
|
|
|
/* Get the x, y, dx, dy in paintable coords */
|
|
|
|
x = (start_x + offset_x - area->image.x) / area->scale;
|
|
|
|
y = (start_y + offset_y - area->image.y) / area->scale;
|
|
|
|
delta_x = (offset_x - area->drag_offx) / area->scale;
|
|
|
|
delta_y = (offset_y - area->drag_offy) / area->scale;
|
|
|
|
|
|
|
|
/* Helper variables */
|
|
|
|
left = area->crop.x;
|
|
|
|
right = area->crop.x + area->crop.width - 1;
|
|
|
|
top = area->crop.y;
|
|
|
|
bottom = area->crop.y + area->crop.height - 1;
|
|
|
|
|
2023-06-09 00:48:05 +02:00
|
|
|
center_x = (left + right) / 2;
|
|
|
|
center_y = (top + bottom) / 2;
|
|
|
|
|
|
|
|
distance_left = left;
|
|
|
|
distance_right = gdk_paintable_get_intrinsic_width (area->paintable) - (right + 1);
|
|
|
|
distance_top = top;
|
|
|
|
distance_bottom = gdk_paintable_get_intrinsic_height (area->paintable) - (bottom + 1);
|
|
|
|
|
|
|
|
closest_distance_x = MIN (distance_left, distance_right);
|
|
|
|
closest_distance_y = MIN (distance_top, distance_bottom);
|
|
|
|
|
|
|
|
/* All size variables are center-to-center, not edge-to-edge, hence the missing '+ 1' everywhere */
|
|
|
|
size_x = right - left;
|
|
|
|
size_y = bottom - top;
|
|
|
|
|
|
|
|
min_size = MAX (area->min_crop_width / area->scale, area->min_crop_height / area->scale);
|
2022-01-26 10:13:58 +01:00
|
|
|
|
|
|
|
/* What we have to do depends on where the user started dragging */
|
|
|
|
switch (area->active_region) {
|
|
|
|
case INSIDE:
|
2023-06-09 00:48:05 +02:00
|
|
|
if (delta_x < 0)
|
|
|
|
clamped_delta_x = MAX (delta_x, -distance_left);
|
|
|
|
else
|
|
|
|
clamped_delta_x = MIN (delta_x, distance_right);
|
|
|
|
|
|
|
|
if (delta_y < 0)
|
|
|
|
clamped_delta_y = MAX (delta_y, -distance_top);
|
|
|
|
else
|
|
|
|
clamped_delta_y = MIN (delta_y, distance_bottom);
|
|
|
|
|
|
|
|
left += clamped_delta_x;
|
|
|
|
right += clamped_delta_x;
|
|
|
|
top += clamped_delta_y;
|
|
|
|
bottom += clamped_delta_y;
|
2010-10-30 16:14:30 -04:00
|
|
|
|
2022-01-26 10:13:58 +01:00
|
|
|
break;
|
|
|
|
|
2023-06-09 00:48:05 +02:00
|
|
|
/* The wanted size assumes one side remains glued to the cursor */
|
2022-01-26 10:13:58 +01:00
|
|
|
case TOP_LEFT:
|
2023-06-09 00:48:05 +02:00
|
|
|
max_size = MIN (size_y + distance_top, size_x + distance_left);
|
|
|
|
wanted_size = MAX (bottom - y, right - x);
|
|
|
|
new_size = CLAMP (wanted_size, MIN (min_size, max_size), max_size);
|
|
|
|
top = bottom - new_size;
|
|
|
|
left = right - new_size;
|
2022-01-26 10:13:58 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case TOP:
|
2023-06-09 00:48:05 +02:00
|
|
|
max_size = MIN (size_y + distance_top, size_x + 2 * closest_distance_x);
|
|
|
|
wanted_size = bottom - y;
|
|
|
|
new_size = CLAMP (wanted_size, MIN (min_size, max_size), max_size);
|
|
|
|
top = bottom - new_size;
|
|
|
|
left = center_x - new_size / 2;
|
|
|
|
right = left + new_size;
|
2022-01-26 10:13:58 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case TOP_RIGHT:
|
2023-06-09 00:48:05 +02:00
|
|
|
max_size = MIN (size_y + distance_top, size_x + distance_right);
|
|
|
|
wanted_size = MAX (bottom - y, x - left);
|
|
|
|
new_size = CLAMP (wanted_size, MIN (min_size, max_size), max_size);
|
|
|
|
top = bottom - new_size;
|
|
|
|
right = left + new_size;
|
2022-01-26 10:13:58 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case LEFT:
|
2023-06-09 00:48:05 +02:00
|
|
|
max_size = MIN (size_x + distance_left, size_y + 2 * closest_distance_y);
|
|
|
|
wanted_size = right - x;
|
|
|
|
new_size = CLAMP (wanted_size, MIN (min_size, max_size), max_size);
|
|
|
|
left = right - new_size;
|
|
|
|
top = center_y - new_size / 2;
|
|
|
|
bottom = top + new_size;
|
2022-01-26 10:13:58 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case BOTTOM_LEFT:
|
2023-06-09 00:48:05 +02:00
|
|
|
max_size = MIN (size_y + distance_bottom, size_x + distance_left);
|
|
|
|
wanted_size = MAX (y - top, right - x);
|
|
|
|
new_size = CLAMP (wanted_size, MIN (min_size, max_size), max_size);
|
|
|
|
bottom = top + new_size;
|
|
|
|
left = right - new_size;
|
2022-01-26 10:13:58 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case RIGHT:
|
2023-06-09 00:48:05 +02:00
|
|
|
max_size = MIN (size_x + distance_right, size_y + 2 * closest_distance_y);
|
|
|
|
wanted_size = x - left;
|
|
|
|
new_size = CLAMP (wanted_size, MIN (min_size, max_size), max_size);
|
|
|
|
right = left + new_size;
|
|
|
|
top = center_y - new_size / 2;
|
|
|
|
bottom = top + new_size;
|
2022-01-26 10:13:58 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case BOTTOM_RIGHT:
|
2023-06-09 00:48:05 +02:00
|
|
|
max_size = MIN (size_y + distance_bottom, size_x + distance_right);
|
|
|
|
wanted_size = MAX (y - top, x - left);
|
|
|
|
new_size = CLAMP (wanted_size, MIN (min_size, max_size), max_size);
|
|
|
|
bottom = top + new_size;
|
|
|
|
right = left + new_size;
|
2022-01-26 10:13:58 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
case BOTTOM:
|
2023-06-09 00:48:05 +02:00
|
|
|
max_size = MIN (size_y + distance_bottom, size_x + 2 * closest_distance_x);
|
|
|
|
wanted_size = y - top;
|
|
|
|
new_size = CLAMP (wanted_size, MIN (min_size, max_size), max_size);
|
|
|
|
bottom = top + new_size;
|
|
|
|
left = center_x - new_size / 2;
|
|
|
|
right = left + new_size;
|
2022-01-26 10:13:58 +01:00
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
area->crop.x = left;
|
|
|
|
area->crop.y = top;
|
|
|
|
area->crop.width = right - left + 1;
|
|
|
|
area->crop.height = bottom - top + 1;
|
2010-10-30 16:14:30 -04:00
|
|
|
|
2023-06-09 00:48:05 +02:00
|
|
|
/* Only update drag_off based on the rounded deltas, otherwise rounding accumulates */
|
|
|
|
area->drag_offx += area->scale * delta_x;
|
|
|
|
area->drag_offy += area->scale * delta_y;
|
2010-10-30 16:14:30 -04:00
|
|
|
|
2022-01-26 10:13:58 +01:00
|
|
|
gtk_widget_queue_draw (GTK_WIDGET (area));
|
2010-10-30 16:14:30 -04:00
|
|
|
}
|
|
|
|
|
2022-01-26 10:13:58 +01:00
|
|
|
static void
|
2023-05-31 10:36:27 +12:00
|
|
|
on_drag_end (CcCropArea *area,
|
2022-01-26 10:13:58 +01:00
|
|
|
double offset_x,
|
2023-05-31 10:36:27 +12:00
|
|
|
double offset_y)
|
2010-10-30 16:14:30 -04:00
|
|
|
{
|
2022-01-26 10:13:58 +01:00
|
|
|
area->active_region = OUTSIDE;
|
|
|
|
area->drag_offx = 0.0;
|
|
|
|
area->drag_offy = 0.0;
|
|
|
|
}
|
2010-10-30 16:14:30 -04:00
|
|
|
|
2022-01-26 10:13:58 +01:00
|
|
|
static void
|
2023-05-31 10:36:27 +12:00
|
|
|
on_drag_cancel (CcCropArea *area,
|
|
|
|
GdkEventSequence *sequence)
|
2022-01-26 10:13:58 +01:00
|
|
|
{
|
|
|
|
area->active_region = OUTSIDE;
|
|
|
|
area->drag_offx = 0;
|
|
|
|
area->drag_offy = 0;
|
2010-10-30 16:14:30 -04:00
|
|
|
}
|
|
|
|
|
2023-06-09 00:04:31 +02:00
|
|
|
#define CORNER_LINE_WIDTH 4.0
|
|
|
|
#define CORNER_LINE_LENGTH 15.0
|
|
|
|
#define CORNER_SIZE (CORNER_LINE_LENGTH + CORNER_LINE_WIDTH / 2)
|
|
|
|
|
2014-11-28 19:38:32 +01:00
|
|
|
static void
|
2022-01-26 10:13:58 +01:00
|
|
|
cc_crop_area_snapshot (GtkWidget *widget,
|
|
|
|
GtkSnapshot *snapshot)
|
2014-11-28 19:38:32 +01:00
|
|
|
{
|
2022-01-26 10:13:58 +01:00
|
|
|
CcCropArea *area = CC_CROP_AREA (widget);
|
|
|
|
cairo_t *cr;
|
|
|
|
GdkRectangle crop;
|
|
|
|
|
|
|
|
if (area->paintable == NULL)
|
|
|
|
return;
|
|
|
|
|
|
|
|
update_image_and_crop (area);
|
|
|
|
|
|
|
|
|
|
|
|
gtk_snapshot_save (snapshot);
|
|
|
|
|
|
|
|
/* First draw the picture */
|
|
|
|
gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (area->image.x, area->image.y));
|
|
|
|
|
|
|
|
gdk_paintable_snapshot (area->paintable, snapshot, area->image.width, area->image.height);
|
|
|
|
|
|
|
|
/* Draw the cropping UI on top with cairo */
|
|
|
|
cr = gtk_snapshot_append_cairo (snapshot, &GRAPHENE_RECT_INIT (0, 0, area->image.width, area->image.height));
|
|
|
|
|
|
|
|
get_scaled_crop (area, &crop);
|
|
|
|
crop.x -= area->image.x;
|
|
|
|
crop.y -= area->image.y;
|
|
|
|
|
users: Improve rounding of avatar crop edges
If crop.x changes due to a drag, crop.x + crop.width remains fixed, as
thet is the opposite edge. However, in the mapping of paintable to
widget coordinates, crop->x + crop->width can vary due to rounding
errors. This fixes that, so that crop->x + crop->width does not vary,
with the same fix in the y direction.
However, the edges of the circle can still remain jittery due to integer
rounding, which is not fixed by using 'width / 2.0' instead of 'width /
2', since the width and height might differ by a pixel. Instead, draw an
ellipse, which removes edge jitter completely.
Part-of: <https://gitlab.gnome.org/GNOME/gnome-control-center/-/merge_requests/1824>
2023-06-08 23:38:06 +02:00
|
|
|
/* Draw the circle as an ellipse, to prevent rounding from jitter of the edges */
|
|
|
|
cairo_save (cr);
|
|
|
|
cairo_translate (cr, crop.x + crop.width / 2.0, crop.y + crop.height / 2.0);
|
|
|
|
cairo_scale (cr, crop.width / 2.0, crop.height / 2.0);
|
|
|
|
cairo_arc (cr, 0, 0, 1, 0, 2 * G_PI);
|
|
|
|
cairo_restore (cr);
|
2022-01-26 10:13:58 +01:00
|
|
|
cairo_save (cr);
|
|
|
|
cairo_rectangle (cr, 0, 0, area->image.width, area->image.height);
|
|
|
|
cairo_set_source_rgba (cr, 0, 0, 0, 0.4);
|
|
|
|
cairo_set_fill_rule (cr, CAIRO_FILL_RULE_EVEN_ODD);
|
|
|
|
cairo_fill (cr);
|
|
|
|
cairo_restore (cr);
|
|
|
|
|
|
|
|
/* draw the four corners */
|
|
|
|
cairo_set_source_rgb (cr, 1, 1, 1);
|
2023-06-09 00:04:31 +02:00
|
|
|
cairo_set_line_width (cr, CORNER_LINE_WIDTH);
|
2022-01-26 10:13:58 +01:00
|
|
|
|
|
|
|
/* top left corner */
|
2023-06-09 00:04:31 +02:00
|
|
|
cairo_move_to (cr, crop.x + CORNER_LINE_WIDTH / 2, crop.y + CORNER_SIZE);
|
|
|
|
cairo_rel_line_to (cr, 0, -CORNER_LINE_LENGTH);
|
|
|
|
cairo_rel_line_to (cr, CORNER_LINE_LENGTH, 0);
|
2022-01-26 10:13:58 +01:00
|
|
|
/* top right corner */
|
2023-06-09 00:04:31 +02:00
|
|
|
cairo_rel_move_to (cr, crop.width - 2 * CORNER_SIZE, 0);
|
|
|
|
cairo_rel_line_to (cr, CORNER_LINE_LENGTH, 0);
|
|
|
|
cairo_rel_line_to (cr, 0, CORNER_LINE_LENGTH);
|
2022-01-26 10:13:58 +01:00
|
|
|
/* bottom right corner */
|
2023-06-09 00:04:31 +02:00
|
|
|
cairo_rel_move_to (cr, 0, crop.height - 2 * CORNER_SIZE);
|
|
|
|
cairo_rel_line_to (cr, 0, CORNER_LINE_LENGTH);
|
|
|
|
cairo_rel_line_to (cr, -CORNER_LINE_LENGTH, 0);
|
2022-01-26 10:13:58 +01:00
|
|
|
/* bottom left corner */
|
2023-06-09 00:04:31 +02:00
|
|
|
cairo_rel_move_to (cr, -(crop.width - 2 * CORNER_SIZE), 0);
|
|
|
|
cairo_rel_line_to (cr, -CORNER_LINE_LENGTH, 0);
|
|
|
|
cairo_rel_line_to (cr, 0, -CORNER_LINE_LENGTH);
|
2022-01-26 10:13:58 +01:00
|
|
|
|
|
|
|
cairo_stroke (cr);
|
|
|
|
|
|
|
|
gtk_snapshot_restore (snapshot);
|
2014-11-28 19:38:32 +01:00
|
|
|
}
|
|
|
|
|
2010-10-30 16:14:30 -04:00
|
|
|
static void
|
2014-10-15 13:40:08 +01:00
|
|
|
cc_crop_area_finalize (GObject *object)
|
2010-10-30 16:14:30 -04:00
|
|
|
{
|
2022-01-26 10:13:58 +01:00
|
|
|
CcCropArea *area = CC_CROP_AREA (object);
|
2019-10-14 09:55:04 +02:00
|
|
|
|
2022-01-26 10:13:58 +01:00
|
|
|
g_clear_object (&area->paintable);
|
2010-10-30 16:14:30 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2014-10-15 13:40:08 +01:00
|
|
|
cc_crop_area_class_init (CcCropAreaClass *klass)
|
2010-10-30 16:14:30 -04:00
|
|
|
{
|
2022-01-26 10:13:58 +01:00
|
|
|
GObjectClass *object_class = G_OBJECT_CLASS (klass);
|
|
|
|
GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
|
|
|
|
|
|
|
|
object_class->finalize = cc_crop_area_finalize;
|
|
|
|
|
|
|
|
widget_class->snapshot = cc_crop_area_snapshot;
|
2010-10-30 16:14:30 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
static void
|
2014-10-15 13:40:08 +01:00
|
|
|
cc_crop_area_init (CcCropArea *area)
|
2010-10-30 16:14:30 -04:00
|
|
|
{
|
2022-01-26 10:13:58 +01:00
|
|
|
GtkGesture *gesture;
|
|
|
|
GtkEventController *controller;
|
|
|
|
|
|
|
|
/* Add handlers for dragging */
|
|
|
|
gesture = gtk_gesture_drag_new ();
|
2023-05-31 10:36:27 +12:00
|
|
|
g_signal_connect_swapped (gesture, "drag-begin", G_CALLBACK (on_drag_begin), area);
|
|
|
|
g_signal_connect_swapped (gesture, "drag-update", G_CALLBACK (on_drag_update), area);
|
|
|
|
g_signal_connect_swapped (gesture, "drag-end", G_CALLBACK (on_drag_end), area);
|
|
|
|
g_signal_connect_swapped (gesture, "cancel", G_CALLBACK (on_drag_cancel), area);
|
2022-01-26 10:13:58 +01:00
|
|
|
gtk_widget_add_controller (GTK_WIDGET (area), GTK_EVENT_CONTROLLER (gesture));
|
|
|
|
|
|
|
|
/* Add handlers for motion events */
|
|
|
|
controller = gtk_event_controller_motion_new ();
|
2023-05-31 10:36:27 +12:00
|
|
|
g_signal_connect_swapped (controller, "motion", G_CALLBACK (on_motion), area);
|
|
|
|
g_signal_connect_swapped (controller, "leave", G_CALLBACK (on_leave), area);
|
2022-01-26 10:13:58 +01:00
|
|
|
gtk_widget_add_controller (GTK_WIDGET (area), GTK_EVENT_CONTROLLER (controller));
|
|
|
|
|
|
|
|
area->scale = 0.0;
|
|
|
|
area->image.x = 0;
|
|
|
|
area->image.y = 0;
|
|
|
|
area->image.width = 0;
|
|
|
|
area->image.height = 0;
|
|
|
|
area->active_region = OUTSIDE;
|
|
|
|
area->min_crop_width = 48;
|
|
|
|
area->min_crop_height = 48;
|
|
|
|
|
|
|
|
gtk_widget_set_size_request (GTK_WIDGET (area), 48, 48);
|
2010-10-30 16:14:30 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
GtkWidget *
|
2014-10-15 13:40:08 +01:00
|
|
|
cc_crop_area_new (void)
|
2010-10-30 16:14:30 -04:00
|
|
|
{
|
2022-01-26 10:13:58 +01:00
|
|
|
return g_object_new (CC_TYPE_CROP_AREA, NULL);
|
2010-10-30 16:14:30 -04:00
|
|
|
}
|
|
|
|
|
2022-01-26 10:13:58 +01:00
|
|
|
/**
|
|
|
|
* cc_crop_area_create_pixbuf:
|
|
|
|
* @area: A crop area
|
|
|
|
*
|
|
|
|
* Renders the area's paintable, with the cropping applied by the user, into a
|
|
|
|
* GdkPixbuf.
|
|
|
|
*
|
|
|
|
* Returns: (transfer full): The cropped picture
|
|
|
|
*/
|
2010-10-30 16:14:30 -04:00
|
|
|
GdkPixbuf *
|
2022-01-26 10:13:58 +01:00
|
|
|
cc_crop_area_create_pixbuf (CcCropArea *area)
|
2010-10-30 16:14:30 -04:00
|
|
|
{
|
2022-01-26 10:13:58 +01:00
|
|
|
g_autoptr (GtkSnapshot) snapshot = NULL;
|
|
|
|
g_autoptr (GskRenderNode) node = NULL;
|
|
|
|
g_autoptr (GskRenderer) renderer = NULL;
|
|
|
|
g_autoptr (GdkTexture) texture = NULL;
|
|
|
|
g_autoptr (GError) error = NULL;
|
|
|
|
graphene_rect_t viewport;
|
|
|
|
|
|
|
|
g_return_val_if_fail (CC_IS_CROP_AREA (area), NULL);
|
|
|
|
|
|
|
|
snapshot = gtk_snapshot_new ();
|
|
|
|
gdk_paintable_snapshot (area->paintable, snapshot,
|
|
|
|
gdk_paintable_get_intrinsic_width (area->paintable),
|
|
|
|
gdk_paintable_get_intrinsic_height (area->paintable));
|
|
|
|
node = gtk_snapshot_free_to_node (g_steal_pointer (&snapshot));
|
|
|
|
|
|
|
|
renderer = gsk_gl_renderer_new ();
|
|
|
|
if (!gsk_renderer_realize (renderer, NULL, &error)) {
|
|
|
|
g_warning ("Couldn't realize GL renderer: %s", error->message);
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
viewport = GRAPHENE_RECT_INIT (area->crop.x, area->crop.y,
|
|
|
|
area->crop.width, area->crop.height);
|
|
|
|
texture = gsk_renderer_render_texture (renderer, node, &viewport);
|
|
|
|
gsk_renderer_unrealize (renderer);
|
|
|
|
|
|
|
|
return gdk_pixbuf_get_from_texture (texture);
|
|
|
|
}
|
2010-10-30 16:14:30 -04:00
|
|
|
|
2022-01-26 10:13:58 +01:00
|
|
|
/**
|
|
|
|
* cc_crop_area_get_paintable:
|
|
|
|
* @area: A crop area
|
|
|
|
*
|
|
|
|
* Returns the area's paintable, unmodified.
|
|
|
|
*
|
|
|
|
* Returns: (transfer none) (nullable): The paintable which the user can crop
|
|
|
|
*/
|
|
|
|
GdkPaintable *
|
|
|
|
cc_crop_area_get_paintable (CcCropArea *area)
|
|
|
|
{
|
|
|
|
g_return_val_if_fail (CC_IS_CROP_AREA (area), NULL);
|
2010-10-30 16:14:30 -04:00
|
|
|
|
2022-01-26 10:13:58 +01:00
|
|
|
return area->paintable;
|
2010-10-30 16:14:30 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
void
|
2022-01-26 10:13:58 +01:00
|
|
|
cc_crop_area_set_paintable (CcCropArea *area,
|
|
|
|
GdkPaintable *paintable)
|
2010-10-30 16:14:30 -04:00
|
|
|
{
|
2022-01-26 10:13:58 +01:00
|
|
|
g_return_if_fail (CC_IS_CROP_AREA (area));
|
|
|
|
g_return_if_fail (GDK_IS_PAINTABLE (paintable));
|
2010-10-30 16:14:30 -04:00
|
|
|
|
2022-01-26 10:13:58 +01:00
|
|
|
g_set_object (&area->paintable, paintable);
|
2010-10-30 16:14:30 -04:00
|
|
|
|
2022-01-26 10:13:58 +01:00
|
|
|
area->scale = 0.0;
|
|
|
|
area->image.x = 0;
|
|
|
|
area->image.y = 0;
|
|
|
|
area->image.width = 0;
|
|
|
|
area->image.height = 0;
|
2010-10-30 16:14:30 -04:00
|
|
|
|
2022-01-26 10:13:58 +01:00
|
|
|
gtk_widget_queue_draw (GTK_WIDGET (area));
|
2010-10-30 16:14:30 -04:00
|
|
|
}
|
|
|
|
|
2022-01-26 10:13:58 +01:00
|
|
|
/**
|
|
|
|
* cc_crop_area_set_min_size:
|
|
|
|
* @area: A crop widget
|
|
|
|
* @width: The minimal width
|
|
|
|
* @height: The minimal height
|
|
|
|
*
|
|
|
|
* Sets the minimal size of the crop rectangle (in paintable coordinates)
|
|
|
|
*/
|
2010-10-30 16:14:30 -04:00
|
|
|
void
|
2014-10-15 13:40:08 +01:00
|
|
|
cc_crop_area_set_min_size (CcCropArea *area,
|
2022-01-26 10:13:58 +01:00
|
|
|
int width,
|
|
|
|
int height)
|
2010-10-30 16:14:30 -04:00
|
|
|
{
|
2022-01-26 10:13:58 +01:00
|
|
|
g_return_if_fail (CC_IS_CROP_AREA (area));
|
2014-11-28 19:38:32 +01:00
|
|
|
|
2022-01-26 10:13:58 +01:00
|
|
|
area->min_crop_width = width;
|
|
|
|
area->min_crop_height = height;
|
2010-10-30 16:14:30 -04:00
|
|
|
|
2022-01-26 10:13:58 +01:00
|
|
|
gtk_widget_set_size_request (GTK_WIDGET (area),
|
|
|
|
area->min_crop_width,
|
|
|
|
area->min_crop_height);
|
2010-10-30 16:14:30 -04:00
|
|
|
}
|