/* * 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); }