/*
 *  $Id: layer-rectangle.c 29656 2026-03-26 10:21:10Z yeti-dn $
 *  Copyright (C) 2024 David Necas (Yeti), Petr Klapetek.
 *  E-mail: yeti@gwyddion.net, klapetek@gwyddion.net.
 *
 *  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, write to the
 *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include "config.h"
#include <math.h>
#include <gdk/gdkkeysyms.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/math.h"
#include "libgwyddion/selection-rectangle.h"

#include "libgwyui/data-view.h"
#include "libgwyui/layer-rectangle.h"
#include "libgwyui/cairo-utils.h"
#include "libgwyui/layer-utils.h"

enum {
    OBJECT_SIZE = 4
};

enum {
    PROP_0,
    PROP_DRAW_AS_CROP,
    PROP_DRAW_REFLECTION,
    PROP_SNAP_TO_CENTER,
    NUM_PROPERTIES,
};

struct _GwyLayerRectanglePrivate {
    /* Properties */
    gboolean draw_as_crop;
    gboolean draw_reflection;
    gboolean snap_to_centre;

    /* Dynamic state */
    gboolean constrain_to_square;
};

static void set_property                (GObject *object,
                                         guint prop_id,
                                         const GValue *value,
                                         GParamSpec *pspec);
static void get_property                (GObject *object,
                                         guint prop_id,
                                         GValue *value,
                                         GParamSpec *pspec);
static void draw                        (GwyVectorLayer *layer,
                                         cairo_t *cr);
static void draw_or_invalidate          (GwyVectorLayer *layer,
                                         cairo_t *cr,
                                         GwyDataView *dataview,
                                         cairo_region_t *region,
                                         const gdouble *xy);
static void draw_or_invalidate_rectangle(GwyVectorLayer *layer,
                                         cairo_t *cr,
                                         GwyDataView *dataview,
                                         cairo_region_t *region,
                                         const gdouble *xy);
static void invalidate_object           (GwyVectorLayer *layer,
                                         cairo_region_t *region,
                                         const gdouble *xy);
static void pointer_moved               (GwyVectorLayer *layer,
                                         gdouble x,
                                         gdouble y);
static void button_pressed              (GwyVectorLayer *layer,
                                         gdouble x,
                                         gdouble y);
static void button_released             (GwyVectorLayer *layer,
                                         gdouble x,
                                         gdouble y);
static gint near_vertex                 (GwyVectorLayer *layer,
                                         gdouble xreal,
                                         gdouble yreal);
static void enforce_constraints         (GwyDataView *dataview,
                                         gboolean square_aspect,
                                         gboolean snap_to_centre,
                                         gdouble *xy);
//static gboolean key_pressed        (GwyVectorLayer *layer,
//                                    GdkEventKey *event);

static const gchar* corner_cursors[OBJECT_SIZE] = { "nw-resize", "sw-resize", "se-resize", "ne-resize" };

static GParamSpec *properties[NUM_PROPERTIES] = { NULL, };
static GwyVectorLayerClass *parent_class = NULL;

G_DEFINE_TYPE_WITH_CODE(GwyLayerRectangle, gwy_layer_rectangle, GWY_TYPE_VECTOR_LAYER,
                        G_ADD_PRIVATE(GwyLayerRectangle))

static void
gwy_layer_rectangle_class_init(GwyLayerRectangleClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GwyVectorLayerClass *vector_class = GWY_VECTOR_LAYER_CLASS(klass);

    parent_class = gwy_layer_rectangle_parent_class;

    gobject_class->set_property = set_property;
    gobject_class->get_property = get_property;

    vector_class->selection_type = GWY_TYPE_SELECTION_RECTANGLE;
    vector_class->draw = draw;
    vector_class->invalidate_object = invalidate_object;
    vector_class->motion = pointer_moved;
    vector_class->button_press = button_pressed;
    vector_class->button_release = button_released;
    //vector_class->key_press = key_pressed;

    properties[PROP_DRAW_AS_CROP] = g_param_spec_boolean("draw-as-crop", NULL,
                                                         "Whether the selection is drawn crop-style with lines "
                                                         "from border to border instead of plain rectangle",
                                                         FALSE, GWY_GPARAM_RWE);
    properties[PROP_DRAW_REFLECTION] = g_param_spec_boolean("draw-reflection", NULL,
                                                            "Whether central reflection of selection should be "
                                                            "drawn too",
                                                            FALSE, GWY_GPARAM_RWE);
    properties[PROP_SNAP_TO_CENTER] = g_param_spec_boolean("snap-to-center", NULL,
                                                           "Whether the selection centre should be fixed to the image "
                                                           "centre",
                                                           FALSE, GWY_GPARAM_RWE);

    /* TODO: We need also centre-x and centre-y properties. This allows the modules (well, 2D FFT filter anyway) to
     * give clear indication where the centre is and we do not have to guess some possible one-pixel offsets. */

    g_object_class_install_properties(gobject_class, NUM_PROPERTIES, properties);
}

static void
gwy_layer_rectangle_init(GwyLayerRectangle *layer)
{
    layer->priv = gwy_layer_rectangle_get_instance_private(layer);
}

static void
set_property(GObject *object,
             guint prop_id,
             const GValue *value,
             GParamSpec *pspec)
{
    GwyLayerRectangle *layer = GWY_LAYER_RECTANGLE(object);

    switch (prop_id) {
        case PROP_DRAW_AS_CROP:
        gwy_layer_rectangle_set_draw_as_crop(layer, g_value_get_boolean(value));
        break;

        case PROP_DRAW_REFLECTION:
        gwy_layer_rectangle_set_draw_reflection(layer, g_value_get_boolean(value));
        break;

        case PROP_SNAP_TO_CENTER:
        gwy_layer_rectangle_set_snap_to_center(layer, g_value_get_boolean(value));
        break;

        default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static void
get_property(GObject *object,
             guint prop_id,
             GValue *value,
             GParamSpec *pspec)
{
    GwyLayerRectanglePrivate *priv = GWY_LAYER_RECTANGLE(object)->priv;

    switch (prop_id) {
        case PROP_DRAW_AS_CROP:
        g_value_set_boolean(value, priv->draw_as_crop);
        break;

        case PROP_DRAW_REFLECTION:
        g_value_set_boolean(value, priv->draw_reflection);
        break;

        case PROP_SNAP_TO_CENTER:
        g_value_set_boolean(value, priv->snap_to_centre);
        break;

        default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
        break;
    }
}

static void
draw(GwyVectorLayer *layer, cairo_t *cr)
{
    GwySelection *selection = gwy_vector_layer_get_selection(layer);
    GtkWidget *parent = gwy_data_view_layer_get_parent(GWY_DATA_VIEW_LAYER(layer));
    if (!parent) {
        g_warning("Standalone drawing is not implemented yet.");
        return;
    }
    g_return_if_fail(GWY_IS_DATA_VIEW(parent));

    cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
    cairo_set_line_width(cr, 1.0);
    cairo_set_operator(cr, CAIRO_OPERATOR_DIFFERENCE);

    gint n = gwy_selection_get_n_objects(selection);
    for (gint i = 0; i < n; i++) {
        gdouble xy[OBJECT_SIZE];
        gwy_selection_get_object(selection, i, xy);
        draw_or_invalidate(layer, cr, GWY_DATA_VIEW(parent), NULL, xy);
    }

    cairo_stroke(cr);
}

static void
draw_or_invalidate(GwyVectorLayer *layer,
                   cairo_t *cr, GwyDataView *dataview,
                   cairo_region_t *region,
                   const gdouble *xy)
{
    GwyLayerRectanglePrivate *priv = GWY_LAYER_RECTANGLE(layer)->priv;

    draw_or_invalidate_rectangle(layer, cr, dataview, region, xy);
    if (!priv->draw_reflection)
        return;

    gdouble xreal, yreal;
    gwy_data_view_get_real_data_sizes(dataview, &xreal, &yreal);
    gdouble xy_mirror[OBJECT_SIZE] = { xreal - xy[0], yreal - xy[1], xreal - xy[2], yreal - xy[3] };
    draw_or_invalidate_rectangle(layer, cr, dataview, region, xy_mirror);
}

static void
draw_or_invalidate_rectangle(GwyVectorLayer *layer,
                             cairo_t *cr, GwyDataView *dataview,
                             cairo_region_t *region,
                             const gdouble *xy)
{
    GwyLayerRectanglePrivate *priv = GWY_LAYER_RECTANGLE(layer)->priv;
    gdouble x0, y0, x1, y1;

    _gwy_transform_line_to_target(dataview, xy[0], xy[1], xy[2], xy[3], &x0, &y0, &x1, &y1);

    if (!priv->draw_as_crop) {
        if (cr)
            cairo_rectangle(cr, fmin(x0, x1), fmin(y0, y1), fabs(x1 - x0), fabs(y1 - y0));
        if (region)
            gwy_cairo_region_add_erectangle(region, x0, y0, x1, y1, 1.0);
        return;
    }

    gdouble xreal, yreal, xmin, xmax, ymin, ymax;
    gwy_data_view_get_real_data_sizes(dataview, &xreal, &yreal);
    gwy_data_view_coords_real_to_widget(dataview, 0.0, 0.0, &xmin, &ymin);
    gwy_data_view_coords_real_to_widget(dataview, xreal, yreal, &xmax, &ymax);

    gwy_cairo_draw_or_add_thin_line(cr, region, xmin, y0, xmax, y0);
    gwy_cairo_draw_or_add_thin_line(cr, region, xmin, y1, xmax, y1);
    gwy_cairo_draw_or_add_thin_line(cr, region, x0, ymin, x0, ymax);
    gwy_cairo_draw_or_add_thin_line(cr, region, x1, ymin, x1, ymax);
}

static void
invalidate_object(GwyVectorLayer *layer,
                  cairo_region_t *region,
                  const gdouble *xy)
{
    GtkWidget *parent = gwy_data_view_layer_get_parent(GWY_DATA_VIEW_LAYER(layer));
    g_return_if_fail(GWY_IS_DATA_VIEW(parent));
    draw_or_invalidate(layer, NULL, GWY_DATA_VIEW(parent), region, xy);
}

static void
set_cursor_according_to_proximity(GwyVectorLayer *layer, gdouble xreal, gdouble yreal)
{
    gint ipt = near_vertex(layer, xreal, yreal);
    const gchar *name = ipt >= 0 ? corner_cursors[ipt % OBJECT_SIZE] : NULL;
    gwy_data_view_set_named_cursor(GWY_DATA_VIEW(gwy_data_view_layer_get_parent(GWY_DATA_VIEW_LAYER(layer))), name);
}

static void
pointer_moved(GwyVectorLayer *layer, gdouble x, gdouble y)
{
    if (!gwy_vector_layer_get_editable(layer))
        return;

    GwySelection *selection = gwy_vector_layer_get_selection(layer);
    if (!selection)
        return;

    GwyLayerRectanglePrivate *priv = GWY_LAYER_RECTANGLE(layer)->priv;
    GtkWidget *parent = gwy_data_view_layer_get_parent(GWY_DATA_VIEW_LAYER(layer));
    g_return_if_fail(GWY_IS_DATA_VIEW(parent));
    GwyDataView *dataview = GWY_DATA_VIEW(parent);

    gwy_data_view_coords_widget_clamp(dataview, &x, &y);
    gdouble xreal, yreal, xy[OBJECT_SIZE];
    gwy_data_view_coords_widget_to_real(dataview, x, y, &xreal, &yreal);

    if (!gwy_vector_layer_get_current_button(layer)) {
        set_cursor_according_to_proximity(layer, xreal, yreal);
        return;
    }

    gint iobj = gwy_vector_layer_get_current_object(layer);
    if (iobj < 0)
        return;

    gwy_selection_get_object(selection, iobj, xy);
    if (xreal == xy[2] && yreal == xy[3])
        return;

    //g_printerr("current(%d) [%g %g :: %g %g] newxy [%g %g]\n", iobj, xy[0], xy[1], xy[2], xy[3], xreal, yreal);
    xy[2] = xreal;
    xy[3] = yreal;
    priv->constrain_to_square = gwy_vector_layer_get_modifiers(layer) & GDK_SHIFT_MASK;
    enforce_constraints(dataview, priv->constrain_to_square, priv->snap_to_centre, xy);
    gwy_vector_layer_update_object(layer, iobj, xy);
}

static void
button_pressed(GwyVectorLayer *layer, gdouble x, gdouble y)
{
    gint button = gwy_vector_layer_get_current_button(layer);
    if (button != 1)
        return;

    GwySelection *selection = gwy_vector_layer_get_selection(layer);
    GwyLayerRectanglePrivate *priv = GWY_LAYER_RECTANGLE(layer)->priv;
    GtkWidget *parent = gwy_data_view_layer_get_parent(GWY_DATA_VIEW_LAYER(layer));
    g_return_if_fail(GWY_IS_DATA_VIEW(parent));
    GwyDataView *dataview = GWY_DATA_VIEW(parent);

    /* Do nothing when we are outside. */
    if (!gwy_data_view_coords_widget_clamp(dataview, &x, &y))
        return;

    gdouble xreal, yreal;
    gwy_data_view_coords_widget_to_real(dataview, x, y, &xreal, &yreal);
    priv->constrain_to_square = gwy_vector_layer_get_modifiers(layer) & GDK_SHIFT_MASK;

    gint i = near_vertex(layer, xreal, yreal), iobj = (i >= 0 ? i/OBJECT_SIZE : -1);
    if (just_choose_object(layer, iobj))
        return;

    gdouble xy[OBJECT_SIZE];
    if (iobj >= 0) {
        /* The user clicked on a vertex of existing selection. Start moving that vertex (we silently swap coordinates
         * if needed because the vertex being edited is always the second point). */
        gwy_vector_layer_set_current_object(layer, iobj);
        gwy_selection_get_object(selection, iobj, xy);
        xy[0] = (i/2 ? fmin(xy[0], xy[2]) : fmax(xy[0], xy[2]));
        xy[1] = ((i == 1 || i == 2) ? fmin(xy[1], xy[3]) : fmax(xy[1], xy[3]));

        xy[2] = xreal;
        xy[3] = yreal;
    }
    else {
        iobj = -1;
        xy[2] = xy[0] = xreal;
        xy[3] = xy[1] = yreal;
    }
    enforce_constraints(dataview, priv->constrain_to_square, priv->snap_to_centre, xy);
    iobj = gwy_vector_layer_update_object(layer, iobj, xy);
    gwy_vector_layer_set_current_object(layer, iobj);

    /* FIXME: There is no general resize cursor and at this moment we do not know the direction yet. */
    gwy_data_view_set_named_cursor(dataview, "crosshair");
}

static void
button_released(GwyVectorLayer *layer, gdouble x, gdouble y)
{
    gint button = gwy_vector_layer_get_current_button(layer);
    gint iobj = gwy_vector_layer_get_current_object(layer);
    if (button != 1 || iobj < 0)
        return;

    GwySelection *selection = gwy_vector_layer_get_selection(layer);
    GwyLayerRectanglePrivate *priv = GWY_LAYER_RECTANGLE(layer)->priv;
    GtkWidget *parent = gwy_data_view_layer_get_parent(GWY_DATA_VIEW_LAYER(layer));
    g_return_if_fail(GWY_IS_DATA_VIEW(parent));
    GwyDataView *dataview = GWY_DATA_VIEW(parent);

    //gboolean inside = gwy_data_view_coords_widget_clamp(dataview, &x, &y);
    gdouble xy[OBJECT_SIZE];
    gwy_selection_get_object(selection, iobj, xy);
    gwy_data_view_coords_widget_clamp(dataview, &x, &y);

    gdouble xother, yother, xreal, yreal;
    gwy_data_view_coords_widget_to_real(dataview, x, y, &xreal, &yreal);
    gwy_data_view_coords_real_to_widget(dataview, xy[0], xy[1], &xother, &yother);
    gwy_debug("event: [%g, %g], xy: [%g, %g]", x, y, xother, yother);
    /* FIXME: This is kind of logically wrong. We should discard the selection if it does not capture any *image*
     * pixel, not screen pixel. */
    if (fabs(x - xother) < 1.0 || fabs(y - yother) < 1.0)
        gwy_selection_delete_object(selection, iobj);
    else {
        xy[2] = xreal;
        xy[3] = yreal;
        enforce_constraints(dataview, priv->constrain_to_square, priv->snap_to_centre, xy);
        /* Make the final rectangle nicer. */
        GWY_ORDER(gdouble, xy[0], xy[2]);
        GWY_ORDER(gdouble, xy[1], xy[3]);
        gwy_vector_layer_update_object(layer, iobj, xy);
    }

    gwy_vector_layer_set_current_object(layer, -1);
    set_cursor_according_to_proximity(layer, xreal, yreal);
    gwy_selection_finished(selection);
}

/* TODO: Later */
#if 0
static gboolean
key_pressed(GwyVectorLayer *layer, GdkEventKey *event)
{
    gboolean large_step = (event->state & (GDK_CONTROL_MASK | GDK_MOD1_MASK));
    gint which = ((event->state & GDK_SHIFT_MASK) ? 2 : 0);
    GwyDataView *dataview;
    guint keyval = event->keyval;
    gint chosen = priv->chosen, xcurr, ycurr, xnew, ynew, move_distance;
    gdouble xy[OBJECT_SIZE];

    if (chosen < 0
        || chosen >= gwy_selection_get_n_objects(priv->selection))
        return FALSE;

    if (keyval != GDK_KEY_Left && keyval != GDK_KEY_Right
        && keyval != GDK_KEY_Up && keyval != GDK_KEY_Down)
        return FALSE;

    dataview = GWY_DATA_VIEW(GWY_DATA_VIEW_LAYER(layer)->parent);
    g_return_val_if_fail(dataview, FALSE);

    gwy_selection_get_object(priv->selection, chosen, xy);
    gwy_data_view_coords_real_to_widget(dataview, xy[which], xy[which+1], &xcurr, &ycurr);
    xnew = xcurr;
    ynew = ycurr;
    move_distance = (large_step ? 16 : 1);
    if (keyval == GDK_KEY_Left)
        xnew -= move_distance;
    else if (keyval == GDK_KEY_Right)
        xnew += move_distance;
    else if (keyval == GDK_KEY_Up)
        ynew -= move_distance;
    else if (keyval == GDK_KEY_Down)
        ynew += move_distance;
    gwy_data_view_coords_widget_clamp(dataview, &xnew, &ynew);

    if (xnew != xcurr || ynew != ycurr) {
        gwy_data_view_coords_widget_to_real(dataview, xnew, ynew, xy+which, xy+which+1);
        gwy_selection_set_object(priv->selection, chosen, xy);
    }

    return TRUE;
}
#endif

/**
 * gwy_layer_rectangle_new:
 *
 * Creates a new rectangle vector layer.
 *
 * Returns: A newly created rectangle vector layer.
 **/
GwyVectorLayer*
gwy_layer_rectangle_new(void)
{
    return (GwyVectorLayer*)g_object_new(GWY_TYPE_LAYER_RECTANGLE, NULL);
}

/**
 * gwy_layer_rectangle_set_draw_as_crop:
 * @layer: A rectangle vector layer.
 * @draw_as_crop: %TRUE to draw the rectangles with crop lines from the borders; %FALSE for normal rectangles.
 *
 * Sets whether a rectangle vector layer is drawn in the crop style.
 **/
void
gwy_layer_rectangle_set_draw_as_crop(GwyLayerRectangle *layer,
                                     gboolean draw_as_crop)
{
    g_return_if_fail(GWY_IS_LAYER_RECTANGLE(layer));

    GwyLayerRectanglePrivate *priv = layer->priv;
    if (!draw_as_crop == !priv->draw_as_crop)
        return;

    priv->draw_as_crop = !!draw_as_crop;
    g_object_notify_by_pspec(G_OBJECT(layer), properties[PROP_DRAW_AS_CROP]);
    gwy_data_view_layer_updated(GWY_DATA_VIEW_LAYER(layer));
}

/**
 * gwy_layer_rectangle_get_draw_as_crop:
 * @layer: A rectangle vector layer.
 *
 * Reports whether a rectangle vector layer is drawn in the crop style.
 *
 * Returns: %TRUE if rectangles are drawn with crop lines from the borders; %FALSE for normal rectangles.
 **/
gboolean
gwy_layer_rectangle_get_draw_as_crop(GwyLayerRectangle *layer)
{
    g_return_val_if_fail(GWY_IS_LAYER_RECTANGLE(layer), FALSE);
    return layer->priv->draw_as_crop;
}

/**
 * gwy_layer_rectangle_set_draw_reflection:
 * @layer: A rectangle vector layer.
 * @draw_reflection: %TRUE to draw second rectangles, reflected about the image centre; %FALSE for normal rectangles.
 *
 * Sets whether a rectangle vector layer is drawn with a reflection.
 **/
void
gwy_layer_rectangle_set_draw_reflection(GwyLayerRectangle *layer,
                                        gboolean draw_reflection)
{
    g_return_if_fail(GWY_IS_LAYER_RECTANGLE(layer));

    GwyLayerRectanglePrivate *priv = layer->priv;
    if (!draw_reflection == !priv->draw_reflection)
        return;

    priv->draw_reflection = !!draw_reflection;
    g_object_notify_by_pspec(G_OBJECT(layer), properties[PROP_DRAW_REFLECTION]);
    gwy_data_view_layer_updated(GWY_DATA_VIEW_LAYER(layer));
}

/**
 * gwy_layer_rectangle_get_draw_reflection:
 * @layer: A rectangle vector layer.
 *
 * Reports whether a rectangle vector layer is drawn with reflection.
 *
 * Returns: %TRUE if second rectangles are drawn, reflected about the image centre; %FALSE for normal rectangles.
 **/
gboolean
gwy_layer_rectangle_get_draw_reflection(GwyLayerRectangle *layer)
{
    g_return_val_if_fail(GWY_IS_LAYER_RECTANGLE(layer), FALSE);
    return layer->priv->draw_reflection;
}

/**
 * gwy_layer_rectangle_set_snap_to_center:
 * @layer: A rectangle vector layer.
 * @snap_to_center: %TRUE to fix orectangles centres to the image centre; %FALSE to leave them unconstrained.
 *
 * Sets whether a rectangle vector layer has the centre fixed to the image centre.
 **/
void
gwy_layer_rectangle_set_snap_to_center(GwyLayerRectangle *layer, gboolean snap_to_centre)
{
    g_return_if_fail(GWY_IS_LAYER_RECTANGLE(layer));

    GwyLayerRectanglePrivate *priv = layer->priv;
    if (!snap_to_centre == !priv->snap_to_centre)
        return;

    priv->snap_to_centre = !!snap_to_centre;
    g_object_notify_by_pspec(G_OBJECT(layer), properties[PROP_SNAP_TO_CENTER]);
    gwy_data_view_layer_updated(GWY_DATA_VIEW_LAYER(layer));
}

/**
 * gwy_layer_rectangle_get_snap_to_center:
 * @layer: A rectangle vector layer.
 *
 * Reports whether a rectangle vector layer has the centre fixed to the image centre.
 *
 * Returns: %TRUE if rectangle centres are fixed to the centre; %FALSE if they are unconstrained.
 **/
gboolean
gwy_layer_rectangle_get_snap_to_center(GwyLayerRectangle *layer)
{
    g_return_val_if_fail(GWY_IS_LAYER_RECTANGLE(layer), FALSE);
    return layer->priv->snap_to_centre;
}

static int
near_vertex(GwyVectorLayer *layer, gdouble xreal, gdouble yreal)
{
    gdouble d2min;
    gint i = gwy_vector_layer_find_nearest(layer, gwy_math_find_nearest_corner, 1, xreal, yreal, &d2min);
    return (d2min > PROXIMITY_DISTANCE*PROXIMITY_DISTANCE) ? -1 : i;
}

static void
enforce_constraints(GwyDataView *dataview, gboolean square_aspect, gboolean snap_to_centre,
                    gdouble *xy)
{
    gdouble qx = gwy_data_view_get_xmeasure(dataview);
    gdouble qy = gwy_data_view_get_ymeasure(dataview);
    gdouble xreal, yreal;
    gwy_data_view_get_real_data_sizes(dataview, &xreal, &yreal);

    //g_printerr("BEFORE enforce [%g %g :: %g %g]\n", xy[0], xy[1], xy[2], xy[3]);

    /* The square aspect is enforced on screen. This is actually smart because the user can switch how the data are
     * displayed. So we do not need any options for different types of square (pixel, physical). */
    if (snap_to_centre) {
        xy[0] = xreal - xy[2];
        xy[1] = yreal - xy[3];
    }

    if (square_aspect) {
        /* We constrain screen aspect to square, but limit maximum size in real coordinates. */
        gdouble dx = xy[2] - xy[0], dy = xy[3] - xy[1];
        gdouble scrlen = fmin(fabs(dx)/qx, fabs(dy)/qy);
        dx = copysign(qx*scrlen, dx);
        dy = copysign(qy*scrlen, dy);

        if (snap_to_centre) {
            xy[0] = xreal - 0.5*dx;
            xy[1] = yreal - 0.5*dy;
        }
        xy[2] = xy[0] + dx;
        xy[3] = xy[1] + dy;
    }
    //g_printerr("AFTER enforce [%g %g :: %g %g]\n", xy[0], xy[1], xy[2], xy[3]);
}

/**
 * SECTION:layer-rectangle
 * @title: GwyLayerRectangle
 * @short_description: Data view layer for rectangular selections
 *
 * #GwyLayerRectangle allows selection of rectangles oriented along the cardinal axes, corresponding to rectangular
 * image areas which can be passed to #GwyField functions. It uses #GwySelectionRectangle selection type.
 **/

/* vim: set cin columns=120 tw=118 et ts=4  sw=4 cino=>1s,e0,n0,f0,{0,}0,^0,\:1s,=0,g1s,h0,t0,+1s,c3,(0,u0 : */
