From ae1c76ff830d146d41e88d6fba724c0a54bce868 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:45:20 +0200 Subject: Adding upstream version 1:43.6. Signed-off-by: Daniel Baumann --- panels/user-accounts/cc-crop-area.c | 717 ++++++++++++++++++++++++++++++++++++ 1 file changed, 717 insertions(+) create mode 100644 panels/user-accounts/cc-crop-area.c (limited to 'panels/user-accounts/cc-crop-area.c') diff --git a/panels/user-accounts/cc-crop-area.c b/panels/user-accounts/cc-crop-area.c new file mode 100644 index 0000000..a644b60 --- /dev/null +++ b/panels/user-accounts/cc-crop-area.c @@ -0,0 +1,717 @@ +/* + * Copyright 2021 Red Hat, Inc, + * + * Authors: + * - Matthias Clasen + * - Niels De Graef + * + * 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 + * the Free Software Foundation; either version 2 of the License, or + * (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 + * along with this program; if not, see . + */ + +#include "config.h" + +#include +#include +#include +#include + +#include "cc-crop-area.h" + +/** + * 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; + +struct _CcCropArea { + GtkWidget parent_instance; + + GdkPaintable *paintable; + + double scale; /* scale factor to go from paintable size to widget size */ + + const char *current_cursor; + Location active_region; + double drag_offx; + double drag_offy; + + /* 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); + +static void +update_image_and_crop (CcCropArea *area) +{ + GtkAllocation allocation; + int width, height; + int dest_width, dest_height; + double scale; + + if (area->paintable == NULL) + return; + + gtk_widget_get_allocation (GTK_WIDGET (area), &allocation); + + /* Get the size of the paintable */ + width = gdk_paintable_get_intrinsic_width (area->paintable); + height = gdk_paintable_get_intrinsic_height (area->paintable); + + /* 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; + crop->width = area->crop.width * area->scale; + crop->height = area->crop.height * area->scale; +} + +typedef enum { + BELOW, + LOWER, + BETWEEN, + UPPER, + ABOVE +} Range; + +static Range +find_range (int x, + int min, + int max) +{ + 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; +} + +/* Finds the location of (@x, @y) relative to the crop @rect */ +static Location +find_location (GdkRectangle *rect, + int x, + int y) +{ + 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]; +} + +static void +update_cursor (CcCropArea *area, + int x, + int y) +{ + 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; + } + cursor = gdk_cursor_new_from_name (cursor_type, NULL); + gdk_surface_set_cursor (gtk_native_get_surface (native), cursor); + area->current_cursor = cursor_type; + } +} + +static int +eval_radial_line (double center_x, double center_y, + double bounds_x, double bounds_y, + double user_x) +{ + double decision_slope; + double decision_intercept; + + decision_slope = (bounds_y - center_y) / (bounds_x - center_x); + decision_intercept = -(decision_slope * bounds_x); + + return (int) (decision_slope * user_x + decision_intercept); +} + +static gboolean +on_motion (GtkEventControllerMotion *controller, + double event_x, + double event_y, + void *user_data) +{ + CcCropArea *area = CC_CROP_AREA (user_data); + + if (area->paintable == NULL) + return FALSE; + + update_cursor (area, event_x, event_y); + + return FALSE; +} + +static void +on_leave (GtkEventControllerMotion *controller, + void *user_data) +{ + CcCropArea *area = CC_CROP_AREA (user_data); + + if (area->paintable == NULL) + return; + + /* Restore 'default' cursor */ + update_cursor (area, 0, 0); +} + +static void +on_drag_begin (GtkGestureDrag *gesture, + double start_x, + double start_y, + void *user_data) +{ + CcCropArea *area = CC_CROP_AREA (user_data); + GdkRectangle crop; + + if (area->paintable == NULL) + return; + + update_cursor (area, start_x, start_y); + + 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; +} + +static void +on_drag_update (GtkGestureDrag *gesture, + double offset_x, + double offset_y, + void *user_data) +{ + CcCropArea *area = CC_CROP_AREA (user_data); + double start_x, start_y; + int x, y, delta_x, delta_y; + int width, height; + int adj_width, adj_height; + int pb_width, pb_height; + int left, right, top, bottom; + double new_width, new_height; + double center_x, center_y; + int min_width, min_height; + + pb_width = gdk_paintable_get_intrinsic_width (area->paintable); + pb_height = gdk_paintable_get_intrinsic_height (area->paintable); + + 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; + + center_x = (left + right) / 2.0; + center_y = (top + bottom) / 2.0; + + /* What we have to do depends on where the user started dragging */ + switch (area->active_region) { + case INSIDE: + width = right - left + 1; + height = bottom - top + 1; + + left = MAX (left + delta_x, 0); + right = MIN (right + delta_x, pb_width); + top = MAX (top + delta_y, 0); + bottom = MIN (bottom + delta_y, pb_height); + + adj_width = right - left + 1; + adj_height = bottom - top + 1; + if (adj_width != width) { + if (delta_x < 0) + right = left + width - 1; + else + left = right - width + 1; + } + if (adj_height != height) { + if (delta_y < 0) + bottom = top + height - 1; + else + top = bottom - height + 1; + } + + break; + + case TOP_LEFT: + if (y < eval_radial_line (center_x, center_y, left, top, x)) { + top = y; + new_width = bottom - top; + left = right - new_width; + } else { + left = x; + new_height = right - left; + top = bottom - new_height; + } + break; + + case TOP: + top = y; + new_width = bottom - top; + right = left + new_width; + break; + + case TOP_RIGHT: + if (y < eval_radial_line (center_x, center_y, right, top, x)) { + top = y; + new_width = bottom - top; + right = left + new_width; + } else { + right = x; + new_height = right - left; + top = bottom - new_height; + } + break; + + case LEFT: + left = x; + new_height = right - left; + bottom = top + new_height; + break; + + case BOTTOM_LEFT: + if (y < eval_radial_line (center_x, center_y, left, bottom, x)) { + left = x; + new_height = right - left; + bottom = top + new_height; + } else { + bottom = y; + new_width = bottom - top; + left = right - new_width; + } + break; + + case RIGHT: + right = x; + new_height = right - left; + bottom = top + new_height; + break; + + case BOTTOM_RIGHT: + if (y < eval_radial_line (center_x, center_y, right, bottom, x)) { + right = x; + new_height = right - left; + bottom = top + new_height; + } else { + bottom = y; + new_width = bottom - top; + right = left + new_width; + } + break; + + case BOTTOM: + bottom = y; + new_width = bottom - top; + right= left + new_width; + break; + + default: + return; + } + + min_width = area->min_crop_width / area->scale; + min_height = area->min_crop_height / area->scale; + + width = right - left + 1; + height = bottom - top + 1; + if (left < 0 || top < 0 || + right > pb_width || bottom > pb_height || + width < min_width || height < min_height) { + left = area->crop.x; + right = area->crop.x + area->crop.width - 1; + top = area->crop.y; + bottom = area->crop.y + area->crop.height - 1; + } + + area->crop.x = left; + area->crop.y = top; + area->crop.width = right - left + 1; + area->crop.height = bottom - top + 1; + + area->drag_offx = offset_x; + area->drag_offy = offset_y; + + gtk_widget_queue_draw (GTK_WIDGET (area)); +} + +static void +on_drag_end (GtkGestureDrag *gesture, + double offset_x, + double offset_y, + void *user_data) +{ + CcCropArea *area = CC_CROP_AREA (user_data); + + area->active_region = OUTSIDE; + area->drag_offx = 0.0; + area->drag_offy = 0.0; +} + +static void +on_drag_cancel (GtkGesture *gesture, + GdkEventSequence *sequence, + void *user_data) +{ + CcCropArea *area = CC_CROP_AREA (user_data); + + area->active_region = OUTSIDE; + area->drag_offx = 0; + area->drag_offy = 0; +} + +static void +cc_crop_area_snapshot (GtkWidget *widget, + GtkSnapshot *snapshot) +{ + 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; + + /* Draw the circle */ + cairo_save (cr); + cairo_arc (cr, crop.x + crop.width / 2, crop.y + crop.width / 2, crop.width / 2, 0, 2 * G_PI); + 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); + cairo_set_line_width (cr, 4.0); + + /* top left corner */ + cairo_move_to (cr, crop.x + 15, crop.y); + cairo_line_to (cr, crop.x, crop.y); + cairo_line_to (cr, crop.x, crop.y + 15); + /* top right corner */ + cairo_move_to (cr, crop.x + crop.width - 15, crop.y); + cairo_line_to (cr, crop.x + crop.width, crop.y); + cairo_line_to (cr, crop.x + crop.width, crop.y + 15); + /* bottom right corner */ + cairo_move_to (cr, crop.x + crop.width - 15, crop.y + crop.height); + cairo_line_to (cr, crop.x + crop.width, crop.y + crop.height); + cairo_line_to (cr, crop.x + crop.width, crop.y + crop.height - 15); + /* bottom left corner */ + cairo_move_to (cr, crop.x + 15, crop.y + crop.height); + cairo_line_to (cr, crop.x, crop.y + crop.height); + cairo_line_to (cr, crop.x, crop.y + crop.height - 15); + + cairo_stroke (cr); + + gtk_snapshot_restore (snapshot); +} + +static void +cc_crop_area_finalize (GObject *object) +{ + CcCropArea *area = CC_CROP_AREA (object); + + g_clear_object (&area->paintable); +} + +static void +cc_crop_area_class_init (CcCropAreaClass *klass) +{ + 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; +} + +static void +cc_crop_area_init (CcCropArea *area) +{ + GtkGesture *gesture; + GtkEventController *controller; + + /* Add handlers for dragging */ + gesture = gtk_gesture_drag_new (); + g_signal_connect (gesture, "drag-begin", G_CALLBACK (on_drag_begin), area); + g_signal_connect (gesture, "drag-update", G_CALLBACK (on_drag_update), + area); + g_signal_connect (gesture, "drag-end", G_CALLBACK (on_drag_end), area); + g_signal_connect (gesture, "cancel", G_CALLBACK (on_drag_cancel), area); + gtk_widget_add_controller (GTK_WIDGET (area), GTK_EVENT_CONTROLLER (gesture)); + + /* Add handlers for motion events */ + controller = gtk_event_controller_motion_new (); + g_signal_connect (controller, "motion", G_CALLBACK (on_motion), area); + g_signal_connect (controller, "leave", G_CALLBACK (on_leave), area); + 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); +} + +GtkWidget * +cc_crop_area_new (void) +{ + return g_object_new (CC_TYPE_CROP_AREA, NULL); +} + +/** + * 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 + */ +GdkPixbuf * +cc_crop_area_create_pixbuf (CcCropArea *area) +{ + 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); +} + +/** + * 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); + + return area->paintable; +} + +void +cc_crop_area_set_paintable (CcCropArea *area, + GdkPaintable *paintable) +{ + g_return_if_fail (CC_IS_CROP_AREA (area)); + g_return_if_fail (GDK_IS_PAINTABLE (paintable)); + + g_set_object (&area->paintable, paintable); + + area->scale = 0.0; + area->image.x = 0; + area->image.y = 0; + area->image.width = 0; + area->image.height = 0; + + gtk_widget_queue_draw (GTK_WIDGET (area)); +} + +/** + * 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) + */ +void +cc_crop_area_set_min_size (CcCropArea *area, + int width, + int height) +{ + g_return_if_fail (CC_IS_CROP_AREA (area)); + + area->min_crop_width = width; + area->min_crop_height = height; + + gtk_widget_set_size_request (GTK_WIDGET (area), + area->min_crop_width, + area->min_crop_height); +} -- cgit v1.2.3