/*
 *  $Id: gradient-swatch.c 28801 2025-11-05 11:52:53Z yeti-dn $
 *  Copyright (C) 2024-2025 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 <gtk/gtk.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/math.h"

#include "libgwyui/gradient-swatch.h"
#include "libgwyui/cairo-utils.h"
#include "libgwyui/widget-impl-utils.h"

enum {
    PROP_0,
    PROP_USE_ALPHA,
    PROP_HAS_MARKER,
    PROP_EDITABLE,
    PROP_GRADIENT,
    PROP_ADJUSTMENT,
    NUM_PROPERTIES,
};

struct _GwyGradientSwatchPrivate {
    GdkWindow *event_window;
    gint button;

    GwyGradient *gradient;
    cairo_pattern_t *gradient_pattern;
    gulong gradient_id;
    GtkAdjustment *adj;
    gulong adj_changed_id;
    gulong value_changed_id;
    gdouble x;  /* The normalised position where we have last drawn the marker. */

    /* FIXME: Is it iseful to make it orientable? Keep this hidden for now. */
    GtkPositionType towards;
    gboolean use_alpha : 8;
    gboolean has_alpha : 8;
    gboolean has_marker : 8;
    gboolean editable : 8;
};

static void     dispose                (GObject *object);
static void     finalize               (GObject *object);
static void     set_property           (GObject *object,
                                        guint param_id,
                                        const GValue *value,
                                        GParamSpec *pspec);
static void     get_property           (GObject *object,
                                        guint param_id,
                                        GValue *value,
                                        GParamSpec *pspec);
static void     realize                (GtkWidget *widget);
static void     unrealize              (GtkWidget *widget);
static void     map                    (GtkWidget *widget);
static void     unmap                  (GtkWidget *widget);
static void     get_preferred_width    (GtkWidget *widget,
                                        gint *minimum,
                                        gint *natural);
static void     get_preferred_height   (GtkWidget *widget,
                                        gint *minimum,
                                        gint *natural);
static void     size_allocate          (GtkWidget *widget,
                                        GdkRectangle *allocation);
static gboolean draw                   (GtkWidget *widget,
                                        cairo_t *cr);
static gboolean button_pressed         (GtkWidget *widget,
                                        GdkEventButton *event);
static gboolean button_released        (GtkWidget *widget,
                                        GdkEventButton *event);
static gboolean pointer_moved          (GtkWidget *widget,
                                        GdkEventMotion *event);
static gboolean key_pressed            (GtkWidget *widget,
                                        GdkEventKey *event);
static gboolean scrolled               (GtkWidget *widget,
                                        GdkEventScroll *event);
static gboolean mnemonic_activate      (GtkWidget *widget,
                                        gboolean group_cycling);
static void     gradient_data_changed  (GwyGradientSwatch *swatch);
static void     ensure_gradient_pattern(GwyGradientSwatch *swatch);
static void     adjustment_reconfigured(GwyGradientSwatch *swatch);
static void     value_changed          (GwyGradientSwatch *swatch);
static void     invalidate_marker      (GwyGradientSwatch *swatch);
static gdouble  get_normalized_value   (GwyGradientSwatch *swatch);
static void     set_marker_to_pointer  (GwyGradientSwatch *swatch,
                                        gdouble x,
                                        gdouble y);
static void     set_normalized_position(GwyGradientSwatch *swatch,
                                        gdouble x);

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

G_DEFINE_TYPE_WITH_CODE(GwyGradientSwatch, gwy_gradient_swatch, GTK_TYPE_WIDGET,
                        G_ADD_PRIVATE(GwyGradientSwatch))

static void
gwy_gradient_swatch_class_init(GwyGradientSwatchClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);

    parent_class = gwy_gradient_swatch_parent_class;

    gobject_class->dispose = dispose;
    gobject_class->finalize = finalize;
    gobject_class->get_property = get_property;
    gobject_class->set_property = set_property;

    widget_class->realize = realize;
    widget_class->unrealize = unrealize;
    widget_class->map = map;
    widget_class->unmap = unmap;
    widget_class->draw = draw;
    widget_class->get_preferred_width = get_preferred_width;
    widget_class->get_preferred_height = get_preferred_height;
    widget_class->size_allocate = size_allocate;
    widget_class->button_press_event = button_pressed;
    widget_class->button_release_event = button_released;
    widget_class->motion_notify_event = pointer_moved;
    widget_class->scroll_event = scrolled;
    widget_class->key_press_event = key_pressed;
    widget_class->mnemonic_activate = mnemonic_activate;

    properties[PROP_USE_ALPHA] = g_param_spec_boolean("use-alpha", NULL,
                                                      "Whether to display the color with an alpha value",
                                                      FALSE, GWY_GPARAM_RWE);
    properties[PROP_HAS_MARKER] = g_param_spec_boolean("has-marker", NULL,
                                                       "Whether the swatch has a marker",
                                                       FALSE, GWY_GPARAM_RWE);
    properties[PROP_EDITABLE] = g_param_spec_boolean("editable", NULL,
                                                     "Whether the adjustment is user-editable",
                                                     FALSE, GWY_GPARAM_RWE);
    properties[PROP_GRADIENT] = g_param_spec_object("gradient", NULL,
                                                    "Color gradient to display",
                                                    GWY_TYPE_GRADIENT, GWY_GPARAM_RWE);
    properties[PROP_ADJUSTMENT] = g_param_spec_object("adjustment", NULL,
                                                      "Adjustment used with the marker",
                                                      GTK_TYPE_ADJUSTMENT, GWY_GPARAM_RWE);

    g_object_class_install_properties(gobject_class, NUM_PROPERTIES, properties);
}

static void
gwy_gradient_swatch_init(GwyGradientSwatch *swatch)
{
    GwyGradientSwatchPrivate *priv;

    swatch->priv = priv = gwy_gradient_swatch_get_instance_private(swatch);
    priv->gradient = NULL;
    priv->use_alpha = FALSE;
    priv->towards = GTK_POS_RIGHT;
    priv->x = 0.5;

    gtk_widget_set_has_window(GTK_WIDGET(swatch), FALSE);
}

static void
dispose(GObject *object)
{
    GwyGradientSwatchPrivate *priv = GWY_GRADIENT_SWATCH(object)->priv;

    /* Cannot use g_clear_signal_handler() because gradient can be NULL when gradient_id is zero. */
    if (priv->gradient_id) {
        g_signal_handler_disconnect(priv->gradient, priv->gradient_id);
        priv->gradient_id = 0;
    }
    if (priv->adj_changed_id) {
        g_signal_handler_disconnect(priv->adj, priv->adj_changed_id);
        priv->adj_changed_id = 0;
    }
    if (priv->value_changed_id) {
        g_signal_handler_disconnect(priv->adj, priv->value_changed_id);
        priv->value_changed_id = 0;
    }

    G_OBJECT_CLASS(parent_class)->dispose(object);
}

static void
finalize(GObject *object)
{
    GwyGradientSwatchPrivate *priv = GWY_GRADIENT_SWATCH(object)->priv;

    g_clear_object(&priv->adj);
    g_clear_pointer(&priv->gradient_pattern, cairo_pattern_destroy);

    G_OBJECT_CLASS(parent_class)->finalize(object);
}

static void
set_property(GObject *object,
             guint param_id,
             const GValue *value,
             GParamSpec *pspec)
{
    GwyGradientSwatch *swatch = GWY_GRADIENT_SWATCH(object);

    switch (param_id) {
        case PROP_USE_ALPHA:
        gwy_gradient_swatch_set_use_alpha(swatch, g_value_get_boolean(value));
        break;

        case PROP_HAS_MARKER:
        gwy_gradient_swatch_set_has_marker(swatch, g_value_get_boolean(value));
        break;

        case PROP_EDITABLE:
        gwy_gradient_swatch_set_editable(swatch, g_value_get_boolean(value));
        break;

        case PROP_GRADIENT:
        gwy_gradient_swatch_set_gradient(swatch, g_value_get_object(value));
        break;

        case PROP_ADJUSTMENT:
        gwy_gradient_swatch_set_adjustment(swatch, g_value_get_object(value));
        break;

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

static void
get_property(GObject *object,
             guint param_id,
             GValue *value,
             GParamSpec *pspec)
{
    GwyGradientSwatchPrivate *priv = GWY_GRADIENT_SWATCH(object)->priv;

    switch (param_id) {
        case PROP_USE_ALPHA:
        g_value_set_boolean(value, priv->use_alpha);
        break;

        case PROP_HAS_MARKER:
        g_value_set_boolean(value, priv->has_marker);
        break;

        case PROP_EDITABLE:
        g_value_set_boolean(value, priv->editable);
        break;

        case PROP_GRADIENT:
        g_value_set_object(value, priv->gradient);
        break;

        case PROP_ADJUSTMENT:
        g_value_set_object(value, priv->adj);
        break;

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

static void
realize(GtkWidget *widget)
{
    GwyGradientSwatch *swatch = GWY_GRADIENT_SWATCH(widget);
    GwyGradientSwatchPrivate *priv = swatch->priv;

    parent_class->realize(widget);
    priv->event_window = gwy_create_widget_input_window(widget,
                                                        GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK
                                                        | GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK
                                                        | GDK_POINTER_MOTION_MASK | GDK_SCROLL_MASK);
}

static void
unrealize(GtkWidget *widget)
{
    GwyGradientSwatchPrivate *priv = GWY_GRADIENT_SWATCH(widget)->priv;

    gwy_destroy_widget_input_window(widget, &priv->event_window);

    GTK_WIDGET_CLASS(parent_class)->unrealize(widget);
}

static void
map(GtkWidget *widget)
{
    GwyGradientSwatchPrivate *priv = GWY_GRADIENT_SWATCH(widget)->priv;
    GTK_WIDGET_CLASS(parent_class)->map(widget);
    gdk_window_show(priv->event_window);
}

static void
unmap(GtkWidget *widget)
{
    GwyGradientSwatchPrivate *priv = GWY_GRADIENT_SWATCH(widget)->priv;
    gdk_window_hide(priv->event_window);
    GTK_WIDGET_CLASS(parent_class)->unmap(widget);
}

static void
get_preferred_width(G_GNUC_UNUSED GtkWidget *widget,
                    gint *minimum,
                    gint *natural)
{
    *minimum = 4;
    *natural = 48;
}

static void
get_preferred_height(G_GNUC_UNUSED GtkWidget *widget,
                     gint *minimum,
                     gint *natural)
{
    *minimum = 2;
    *natural = 16;
}

static void
size_allocate(GtkWidget *widget, GdkRectangle *allocation)
{
    GwyGradientSwatch *swatch = GWY_GRADIENT_SWATCH(widget);
    GwyGradientSwatchPrivate *priv = swatch->priv;

    gtk_widget_set_allocation(widget, allocation);

    gwy_debug("allocation %d×%d at (%d,%d)", allocation->width, allocation->height, allocation->x, allocation->y);

    if (priv->event_window)
        gdk_window_move_resize(priv->event_window, allocation->x, allocation->y, allocation->width, allocation->height);
}

static inline gboolean
is_color_bright(gdouble r, gdouble g, gdouble b)
{
    return 0.2126*r + 0.7152*g + 0.0722*b > 0.5;
}

static gboolean
draw(GtkWidget *widget, cairo_t *cr)
{
    GwyGradientSwatch *swatch = GWY_GRADIENT_SWATCH(widget);
    GwyGradientSwatchPrivate *priv = swatch->priv;
    GtkStyleContext *context = gtk_widget_get_style_context(widget);
    GtkStateFlags state = gtk_style_context_get_state(context);
    gboolean is_insensitive = state & GTK_STATE_FLAG_INSENSITIVE;
    gint width = gtk_widget_get_allocated_width(widget);
    gint height = gtk_widget_get_allocated_height(widget);

    cairo_save(cr);
    if (is_insensitive)
        cairo_push_group(cr);

    ensure_gradient_pattern(swatch);

    GdkRectangle allocation;
    gtk_widget_get_allocation(widget, &allocation);
    cairo_matrix_t matrix;
    cairo_matrix_init_scale(&matrix, 1.0/allocation.width, 1.0);
    cairo_pattern_set_matrix(priv->gradient_pattern, &matrix);

    if (priv->use_alpha && priv->has_alpha) {
        /* FIXME: If we want vertical gradients, we need to implement them here. */
        gdouble x = 2.0*allocation.width*allocation.height/(allocation.width + allocation.height);
        gdouble check_size = fmin(0.2*x, pow(x, 2.0/3.0)/1.6);
        gint size = MAX(GWY_ROUND(check_size), 1);
        GwyRGBA dark = { 0.333, 0.333, 0.333, 1.0 }, light = { 0.667, 0.667, 0.667, 1.0 };

        gwy_cairo_checker_rectangle(cr, &light, &dark, width, height, size);
    }
    cairo_set_source(cr, priv->gradient_pattern);
    cairo_rectangle(cr, 0, 0, width, height);
    cairo_fill(cr);

    if (priv->has_marker) {
        GwyGradient *gradient = priv->gradient ? priv->gradient : gwy_inventory_get_default_item(gwy_gradients());
        gdouble size = 0.13*allocation.height;
        priv->x = get_normalized_value(swatch);
        GwyRGBA color;
        gwy_gradient_get_color(gradient, priv->x, &color);

        if (is_color_bright(color.r, color.g, color.b))
            cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
        else
            cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);

        /* FIXME: If we want vertical gradients, we need to implement them here. */
        gdouble pos = priv->x * allocation.width;

        cairo_new_path(cr);

        cairo_move_to(cr, pos - size, 0);
        cairo_line_to(cr, pos, GWY_SQRT3*size);
        cairo_line_to(cr, pos + size, 0);
        cairo_close_path(cr);

        cairo_move_to(cr, pos - size, allocation.height);
        cairo_line_to(cr, pos, allocation.height - GWY_SQRT3*size);
        cairo_line_to(cr, pos + size, allocation.height);
        cairo_close_path(cr);

        cairo_fill(cr);
    }

    if (is_insensitive) {
        cairo_pop_group_to_source(cr);
        cairo_paint_with_alpha(cr, 0.6);
    }
    cairo_restore(cr);

    return FALSE;
}

/**
 * gwy_gradient_swatch_new:
 *
 * Creates a new color swatch.
 *
 * Returns: A new color swatch.
 **/
GtkWidget*
gwy_gradient_swatch_new (void)
{
    return g_object_new(GWY_TYPE_GRADIENT_SWATCH, NULL);
}

/**
 * gwy_gradient_swatch_set_gradient:
 * @swatch: A gradient swatch.
 * @gradient: The gradient to display.
 *
 * Sets the current gradient of a gradient swatch.
 *
 * The gradient can be %NULL. The default gradient is then displayed, although it is not terribly useful.
 **/
void
gwy_gradient_swatch_set_gradient(GwyGradientSwatch *swatch,
                                 GwyGradient *gradient)
{
    g_return_if_fail(GWY_IS_GRADIENT_SWATCH(swatch));

    GwyGradientSwatchPrivate *priv = swatch->priv;
    if (!gwy_set_member_object(swatch, gradient, GWY_TYPE_GRADIENT, &priv->gradient,
                               "data-changed", G_CALLBACK(gradient_data_changed), &priv->gradient_id, G_CONNECT_SWAPPED,
                               NULL))
        return;

    g_clear_pointer(&priv->gradient_pattern, cairo_pattern_destroy);
    if (gtk_widget_is_drawable(GTK_WIDGET(swatch)))
        gtk_widget_queue_draw(GTK_WIDGET(swatch));
    g_object_notify_by_pspec(G_OBJECT(swatch), properties[PROP_GRADIENT]);
}

/**
 * gwy_gradient_swatch_get_gradient:
 * @swatch: A gradient swatch.
 *
 * Obtains the current gradient displayed by a gradient swatch.
 *
 * Returns: The displayed gradient.
 **/
GwyGradient*
gwy_gradient_swatch_get_gradient(GwyGradientSwatch *swatch)
{
    g_return_val_if_fail(GWY_IS_GRADIENT_SWATCH(swatch), NULL);
    return swatch->priv->gradient;
}

/**
 * gwy_gradient_swatch_set_use_alpha:
 * @swatch: A gradient swatch.
 * @use_alpha: %TRUE if gradient swatch should visualise the alpha channel, %FALSE to ignore it.
 *
 * Sets whether a gradient swatch should use the alpha channel.
 **/
void
gwy_gradient_swatch_set_use_alpha(GwyGradientSwatch *swatch,
                                  gboolean use_alpha)
{
    g_return_if_fail(GWY_IS_GRADIENT_SWATCH(swatch));

    GwyGradientSwatchPrivate *priv = swatch->priv;
    if (!priv->use_alpha == !use_alpha)
        return;

    priv->use_alpha = !!use_alpha;
    if (gtk_widget_is_drawable(GTK_WIDGET(swatch)))
        gtk_widget_queue_draw(GTK_WIDGET(swatch));

    g_object_notify_by_pspec(G_OBJECT(swatch), properties[PROP_USE_ALPHA]);
}

/**
 * gwy_gradient_swatch_get_use_alpha:
 * @swatch: A gradient swatch.
 *
 * Reports whether a gradient swatch uses the alpha channel.
 *
 * Returns: %TRUE if the gradient swatch visualises alpha channel, %FALSE if it ignores it.
 **/
gboolean
gwy_gradient_swatch_get_use_alpha(GwyGradientSwatch *swatch)
{
    g_return_val_if_fail(GWY_IS_GRADIENT_SWATCH(swatch), FALSE);
    return swatch->priv->use_alpha;
}

/**
 * gwy_gradient_swatch_set_has_marker:
 * @swatch: A gradient swatch.
 * @has_marker: %TRUE to add an editable marker, %FALSE to remove it.
 *
 * Sets whether a gradient swatch should have a marker.
 *
 * Use gwy_gradient_swatch_set_editable() to make the marker user-editable or passive. By default, the marker is
 * passive.
 **/
void
gwy_gradient_swatch_set_has_marker(GwyGradientSwatch *swatch,
                                   gboolean has_marker)
{
    g_return_if_fail(GWY_IS_GRADIENT_SWATCH(swatch));
    GwyGradientSwatchPrivate *priv = swatch->priv;
    if (!priv->has_marker == !has_marker)
        return;
    priv->has_marker = !!has_marker;

    GtkWidget *widget = GTK_WIDGET(swatch);
    gtk_widget_set_can_focus(widget, has_marker);
    if (has_marker && !priv->adj)
        gwy_gradient_swatch_set_adjustment(swatch, NULL);
    if (gtk_widget_is_drawable(widget))
        gtk_widget_queue_draw(widget);

    g_object_notify_by_pspec(G_OBJECT(swatch), properties[PROP_HAS_MARKER]);
}

/**
 * gwy_gradient_swatch_get_has_marker:
 * @swatch: A gradient swatch.
 *
 * Reports whether a gradient swatch should have a marker.
 *
 * Returns: %TRUE if the swatch has a marker, %FALSE if it does not have any.
 **/
gboolean
gwy_gradient_swatch_get_has_marker(GwyGradientSwatch *swatch)
{
    g_return_val_if_fail(GWY_IS_GRADIENT_SWATCH(swatch), FALSE);
    return swatch->priv->has_marker;
}

/**
 * gwy_gradient_swatch_set_editable:
 * @swatch: A gradient swatch.
 * @editable: %TRUE to let user modify the adjustment value.
 *
 * Sets whether a gradient swatch adjustment is user editable.
 *
 * Making the swatch editable permits the user to move the marker. It is possible, although possibly confusing to
 * the user, to make the swatch editable while keeping the marker disabled. It will respond identically as if the
 * marker was visible.
 **/
void
gwy_gradient_swatch_set_editable(GwyGradientSwatch *swatch,
                                 gboolean editable)
{
    g_return_if_fail(GWY_IS_GRADIENT_SWATCH(swatch));
    GwyGradientSwatchPrivate *priv = swatch->priv;
    if (!priv->editable == !editable)
        return;
    priv->editable = !!editable;

    GtkWidget *widget = GTK_WIDGET(swatch);
    gtk_widget_set_can_focus(widget, editable);
    if (editable && !priv->adj)
        gwy_gradient_swatch_set_adjustment(swatch, NULL);

    /* XXX: If we are not editable we do not need any input window. However, managing it is kind of a hassle because
     * this function can be called in any state of realisation and mapping. */
    g_object_notify_by_pspec(G_OBJECT(swatch), properties[PROP_EDITABLE]);
}

/**
 * gwy_gradient_swatch_get_editable:
 * @swatch: A gradient swatch.
 *
 * Reports whether a gradient swatch is user editable.
 *
 * Returns: %TRUE if the adjustment is user-editable.
 **/
gboolean
gwy_gradient_swatch_get_editable(GwyGradientSwatch *swatch)
{
    g_return_val_if_fail(GWY_IS_GRADIENT_SWATCH(swatch), FALSE);
    return swatch->priv->editable;
}

/**
 * gwy_gradient_swatch_set_adjustment:
 * @swatch: A gradient swatch.
 * @adj: (nullable):
 *       New adjustment to use.
 *
 * Sets the adjustment used for the editable marker of a gradient swatch.
 *
 * Often the implicitly created adjustment is sufficient. If you do not set any adjustment and enable a marker or
 * editability, an implicit adjustment with [0,1] range is created. It is also created when you pass %NULL as @adj
 * and the swatch already has a marker or is editable.
 **/
void
gwy_gradient_swatch_set_adjustment(GwyGradientSwatch *swatch,
                                   GtkAdjustment *adj)
{
    g_return_if_fail(GWY_IS_GRADIENT_SWATCH(swatch));
    GwyGradientSwatchPrivate *priv = swatch->priv;
    if ((priv->has_marker || priv->editable) && !adj)
        adj = gtk_adjustment_new(priv->x, 0.0, 1.0, 0.01, 0.1, 0);
    if (!gwy_set_member_object(swatch, adj, GTK_TYPE_ADJUSTMENT, &priv->adj,
                               "changed", G_CALLBACK(adjustment_reconfigured), &priv->adj_changed_id, G_CONNECT_SWAPPED,
                               "value-changed", G_CALLBACK(value_changed), &priv->value_changed_id, G_CONNECT_SWAPPED,
                               NULL))
        return;

    adjustment_reconfigured(swatch);
    g_object_notify_by_pspec(G_OBJECT(swatch), properties[PROP_ADJUSTMENT]);
}

/**
 * gwy_gradient_swatch_get_adjustment:
 * @swatch: A gradient swatch.
 *
 * Gets the adjustment used for the editable marker of a gradient swatch.
 *
 * Connect to the returned adjustment to watch marker movement. If the gradient swatch has never had an editable
 * marker (nor any adjustment set explicitly) the function can return %NULL.
 *
 * Returns: (nullable):
 *          The adjustment or %NULL.
 **/
GtkAdjustment*
gwy_gradient_swatch_get_adjustment(GwyGradientSwatch *swatch)
{
    g_return_val_if_fail(GWY_IS_GRADIENT_SWATCH(swatch), NULL);
    return swatch->priv->adj;
}

static void
gradient_data_changed(GwyGradientSwatch *swatch)
{
    GwyGradientSwatchPrivate *priv = swatch->priv;

    g_clear_pointer(&priv->gradient_pattern, cairo_pattern_destroy);
    if (gtk_widget_is_drawable(GTK_WIDGET(swatch)))
        gtk_widget_queue_draw(GTK_WIDGET(swatch));
}

static void
ensure_gradient_pattern(GwyGradientSwatch *swatch)
{
    GwyGradientSwatchPrivate *priv = swatch->priv;

    if (priv->gradient_pattern)
        return;

    GwyGradient *gradient = priv->gradient ? priv->gradient : gwy_inventory_get_default_item(gwy_gradients());
    priv->gradient_pattern = gwy_cairo_pattern_create_gradient(gradient, priv->towards, priv->use_alpha);
    priv->has_alpha = FALSE;

    gint npoints;
    const GwyGradientPoint *points = gwy_gradient_get_points(gradient, &npoints);
    for (gint i = 0; i < npoints; i++) {
        if (points[i].color.a < 1.0) {
            priv->has_alpha = TRUE;
            break;
        }
    }
}

static void
adjustment_reconfigured(GwyGradientSwatch *swatch)
{
    invalidate_marker(swatch);
}

static void
value_changed(GwyGradientSwatch *swatch)
{
    invalidate_marker(swatch);
}

static void
invalidate_marker(GwyGradientSwatch *swatch)
{
    GwyGradientSwatchPrivate *priv = swatch->priv;
    GtkWidget *widget = GTK_WIDGET(swatch);
    if (!swatch->priv->has_marker || !gtk_widget_is_drawable(widget))
        return;

    GdkRectangle allocation;
    gtk_widget_get_allocation(widget, &allocation);
    gdouble size = 0.14*allocation.height;
    gdouble oldpos = priv->x * allocation.width;
    gdouble newpos = get_normalized_value(swatch) * allocation.width;

    cairo_region_t *region = cairo_region_create();
    gwy_cairo_region_add_crectangle(region, oldpos, size, size, size, 0.1);
    gwy_cairo_region_add_crectangle(region, oldpos, allocation.height - size, size, size, 0.1);
    gwy_cairo_region_add_crectangle(region, newpos, size, size, size, 0.1);
    gwy_cairo_region_add_crectangle(region, newpos, allocation.height - size, size, size, 0.1);

    cairo_region_translate(region, allocation.x, allocation.y);

    gtk_widget_queue_draw_region(widget, region);
    cairo_region_destroy(region);
}

static gdouble
get_normalized_value(GwyGradientSwatch *swatch)
{
    GtkAdjustment *adj = swatch->priv->adj;
    if (!adj)
        return 0.5;
    gdouble lower = gtk_adjustment_get_lower(adj), upper = gtk_adjustment_get_upper(adj);
    if (lower == upper)
        return 0.5;
    gdouble x = (gtk_adjustment_get_value(adj) - lower)/(upper - lower);
    return fmin(fmax(x, 0.0), 1.0);
}

static gboolean
button_pressed(GtkWidget *widget, GdkEventButton *event)
{
    GwyGradientSwatch *swatch = GWY_GRADIENT_SWATCH(widget);
    GwyGradientSwatchPrivate *priv = swatch->priv;
    if (!priv->editable)
        return FALSE;

    gint button = event->button;

    /* React to left button only */
    if (button != 1 || priv->button)
        return FALSE;

    if (gtk_widget_get_can_focus(widget) && !gtk_widget_has_focus(widget))
        gtk_widget_grab_focus(widget);

    set_marker_to_pointer(swatch, event->x, event->y);
    gtk_grab_add(widget);
    priv->button = button;
    return TRUE;
}

static gboolean
button_released(GtkWidget *widget, GdkEventButton *event)
{
    GwyGradientSwatch *swatch = GWY_GRADIENT_SWATCH(widget);
    GwyGradientSwatchPrivate *priv = swatch->priv;
    if (!priv->editable)
        return FALSE;

    gint button = event->button;

    /* React to left button only */
    if (button != 1 || priv->button != 1)
        return FALSE;

    gtk_grab_remove(widget);
    priv->button = 0;

    set_marker_to_pointer(swatch, event->x, event->y);
    return TRUE;
}

static gboolean
pointer_moved(GtkWidget *widget, GdkEventMotion *event)
{
    GwyGradientSwatch *swatch = GWY_GRADIENT_SWATCH(widget);
    GwyGradientSwatchPrivate *priv = swatch->priv;
    if (!priv->editable)
        return FALSE;

    if (!priv->button)
        return FALSE;

    set_marker_to_pointer(swatch, event->x, event->y);
    return TRUE;
}

static gboolean
key_pressed(GtkWidget *widget, GdkEventKey *event)
{
    GwyGradientSwatch *swatch = GWY_GRADIENT_SWATCH(widget);
    GwyGradientSwatchPrivate *priv = swatch->priv;
    if (!priv->editable)
        return FALSE;

    gdouble x = priv->x, step = 0.01;
    guint keyval = event->keyval;
    if (keyval == GDK_KEY_Left || keyval == GDK_KEY_KP_Left)
        x = fmax(x - step, 0.0);
    else if (keyval == GDK_KEY_Right || keyval == GDK_KEY_KP_Right)
        x = fmin(x + step, 1.0);
    else
        return FALSE;

    if (priv->x != x)
        set_normalized_position(swatch, x);
    return TRUE;
}

static gboolean
scrolled(GtkWidget *widget, GdkEventScroll *event)
{
    GwyGradientSwatch *swatch = GWY_GRADIENT_SWATCH(widget);
    GwyGradientSwatchPrivate *priv = swatch->priv;
    if (!priv->editable)
        return FALSE;

    gdouble x = priv->x, step = 0.01;
    x = fmin(fmax(x - step*event->delta_y, 0.0), 1.0);
    if (priv->x != x)
        set_normalized_position(swatch, x);
    return TRUE;
}

static void
set_marker_to_pointer(GwyGradientSwatch *swatch,
                      gdouble x, G_GNUC_UNUSED gdouble y)
{
    /* FIXME: If we want vertical gradients, we need to implement them here. */
    GdkRectangle allocation;
    gtk_widget_get_allocation(GTK_WIDGET(swatch), &allocation);
    set_normalized_position(swatch, x/allocation.width);
}

static void
set_normalized_position(GwyGradientSwatch *swatch, gdouble x)
{
    GtkAdjustment *adj = swatch->priv->adj;
    gdouble lower = gtk_adjustment_get_lower(adj), upper = gtk_adjustment_get_upper(adj);
    x = x*(upper - lower) + lower;
    gtk_adjustment_set_value(adj, x);
}

static gboolean
mnemonic_activate(GtkWidget *widget, G_GNUC_UNUSED gboolean group_cycling)
{
    if (gtk_widget_get_can_focus(widget) && !gtk_widget_has_focus(widget))
        gtk_widget_grab_focus(widget);

    return TRUE;
}

/**
 * SECTION:gradient-swatch
 * @title: GwyGradientSwatch
 * @short_description: A swatch displaying a colour
 *
 * The widget is a simple rectangle displaying a colour gradient, with some support for visualisation of the alpha
 * value.
 **/

/* 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 : */
