/*
 *  $Id: wheel.c 27391 2025-02-28 13:26:20Z yeti-dn $
 *  Copyright (C) 2003-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 <glib/gi18n-lib.h>
#include <glib-object.h>
#include <gdk/gdkkeysyms.h>
#include <gtk/gtk.h>

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

#include "libgwyui/color-wheel.h"
#include "libgwyui/widget-impl-utils.h"
#include "libgwyui/cairo-utils.h"

enum {
    SGNL_COLOR_CHANGED,
    NUM_SIGNALS
};

typedef enum {
    PIECE_NONE = -1,
    PIECE_WHEEL,
    PIECE_TRIANGLE,
} ColorWheelPiece;

struct _GwyColorWheelPrivate {
    GdkWindow *event_window;
    gint button;

    gint diameter;
    gint inner_diameter;
    ColorWheelPiece editing;

    GwyRGBA color;
    gdouble h;
    gdouble s;
    gdouble v;

    GdkPixbuf *wheel_pixbuf;
};

static void     finalize               (GObject *object);
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 void     paint_wheel            (GwyColorWheel *wheel);
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 void     update_hue             (GwyColorWheel *wheel,
                                        gdouble x,
                                        gdouble y);
static gboolean update_saturation_value(GwyColorWheel *wheel,
                                        gdouble x,
                                        gdouble y,
                                        gboolean only_inside);
static void     update_rgb_from_hsv    (GwyColorWheel *wheel);
static gboolean mnemonic_activate      (GtkWidget *widget,
                                        gboolean group_cycling);
static void     state_flags_changed    (GtkWidget *widget,
                                        GtkStateFlags old_state);

static guint signals[NUM_SIGNALS] = { };
static GtkWidgetClass *parent_class = NULL;

G_DEFINE_TYPE_WITH_CODE(GwyColorWheel, gwy_color_wheel, GTK_TYPE_WIDGET,
                        G_ADD_PRIVATE(GwyColorWheel))

static void
gwy_color_wheel_class_init(GwyColorWheelClass *klass)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
    GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
    GType type = G_TYPE_FROM_CLASS(klass);

    parent_class = gwy_color_wheel_parent_class;

    gobject_class->finalize = finalize;

    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;
    widget_class->state_flags_changed = state_flags_changed;

    /**
     * GwyColorWheel::color-changed:
     * @gwywheel: The #GwyColorWheel which received the signal.
     *
     * The ::color-changed signal is emitted when the color changes.
     **/
    signals[SGNL_COLOR_CHANGED] = g_signal_new("color-changed", type,
                                               G_SIGNAL_RUN_FIRST,
                                               G_STRUCT_OFFSET(GwyColorWheelClass, color_changed),
                                               NULL, NULL,
                                               g_cclosure_marshal_VOID__VOID,
                                               G_TYPE_NONE, 0);
    g_signal_set_va_marshaller(signals[SGNL_COLOR_CHANGED], type, g_cclosure_marshal_VOID__VOIDv);
}

static void
gwy_color_wheel_init(GwyColorWheel *wheel)
{
    GwyColorWheelPrivate *priv;

    priv = wheel->priv = gwy_color_wheel_get_instance_private(wheel);
    priv->editing = PIECE_WHEEL;
    priv->color = (GwyRGBA){ 0.0, 0.0, 0.0, 1.0 };
    gtk_rgb_to_hsv(priv->color.r, priv->color.g, priv->color.b, &priv->h, &priv->s, &priv->v);

    GtkWidget *widget = GTK_WIDGET(wheel);
    gtk_widget_set_has_window(widget, FALSE);
    gtk_widget_set_can_focus(widget, TRUE);
}

/**
 * gwy_color_wheel_new:
 *
 * Creates a new colour wheel.
 *
 * The widget takes up all the space allocated for it.
 *
 * Returns: The new wheel widget.
 **/
GtkWidget*
gwy_color_wheel_new(void)
{
    return gtk_widget_new(GWY_TYPE_COLOR_WHEEL, NULL);
}

static void
finalize(GObject *object)
{
    GwyColorWheelPrivate *priv = GWY_COLOR_WHEEL(object)->priv;

    g_clear_object(&priv->wheel_pixbuf);

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

static void
realize(GtkWidget *widget)
{
    GwyColorWheel *wheel = GWY_COLOR_WHEEL(widget);
    GwyColorWheelPrivate *priv = wheel->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)
{
    GwyColorWheelPrivate *priv = GWY_COLOR_WHEEL(widget)->priv;

    g_clear_object(&priv->wheel_pixbuf);
    priv->diameter = 0;

    gwy_destroy_widget_input_window(widget, &priv->event_window);

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

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

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

/**
 * gwy_color_wheel_set_color:
 * @wheel: A colour wheel.
 * @color: The colour to display as #GwyRGBA.
 *
 * Sets the current colour of a colour wheel.
 *
 * The alpha component is preserved but ignored.
 **/
void
gwy_color_wheel_set_color(GwyColorWheel *wheel,
                          const GwyRGBA *color)
{
    g_return_if_fail(GWY_IS_COLOR_WHEEL(wheel));
    g_return_if_fail(color);
    GwyColorWheelPrivate *priv = wheel->priv;
    if (gwy_rgba_equal(color, &priv->color))
        return;
    priv->color = *color;

    gdouble h = priv->h;
    gtk_rgb_to_hsv(priv->color.r, priv->color.g, priv->color.b, &priv->h, &priv->s, &priv->v);
    /* Preserve wheel rotation by keeping the h when desaturating. */
    if (!priv->s)
        priv->h = h;

    if (gtk_widget_is_drawable(GTK_WIDGET(wheel)))
        gtk_widget_queue_draw(GTK_WIDGET(wheel));
    g_signal_emit(wheel, signals[SGNL_COLOR_CHANGED], 0);
}

/**
 * gwy_color_wheel_get_color:
 * @wheel: A colour wheel.
 * @color: Location to fill in with the current colour.
 *
 * Obtains the current colour of a colour wheel.
 *
 * The alpha component is always whatever was last set using gwy_color_wheel_set_color() because the colour wheel
 * does not have any means to modify alpha.
 **/
void
gwy_color_wheel_get_color(GwyColorWheel *wheel,
                          GwyRGBA *color)
{
    g_return_if_fail(GWY_IS_COLOR_WHEEL(wheel));
    g_return_if_fail(color);
    *color = wheel->priv->color;
}

/**
 * gwy_color_wheel_get_hue:
 * @wheel: A colour wheel.
 *
 * Gets the rotation hue value of a colour wheel.
 *
 * The rotation hue corresponds to the wheel rotation. It is generally the same as the actual hue, except for
 * completely unsaturated (grey) colours. The hue is undefined and conventionally it is defined as 0. However, the
 * wheel tries to avoid sudden changes of orientation when a colour becomes desaturated. Therefore, it may preserve
 * the orientation corresponding to the last colour which has non-zero saturation.
 *
 * Use gwy_color_wheel_get_color() and gtk_rgb_to_hsv() to obtain the conventional hue value. Use this function to
 * obtain the orientation of the wheel if you need it.
 *
 * Returns: The hue value in interval [0,1].
 **/
gdouble
gwy_color_wheel_get_hue(GwyColorWheel *wheel)
{
    g_return_val_if_fail(GWY_IS_COLOR_WHEEL(wheel), 0.0);
    return wheel->priv->h;
}

/**
 * gwy_color_wheel_set_hue:
 * @wheel: A colour wheel.
 * @hue: New hue value in interval [0,1].
 *
 * Sets the rotation hue value of a colour wheel.
 *
 * This function is seldom needed. See gwy_color_wheel_get_hue() for discussion.
 **/
void
gwy_color_wheel_set_hue(GwyColorWheel *wheel,
                        gdouble hue)
{
    g_return_if_fail(GWY_IS_COLOR_WHEEL(wheel));
    gdouble hue_in_01 = gwy_clamp(hue, 0.0, 1.0);
    g_warn_if_fail(hue == hue_in_01);

    GwyColorWheelPrivate *priv = wheel->priv;
    if (priv->h == hue_in_01)
        return;
    priv->h = hue_in_01;
    update_rgb_from_hsv(wheel);
}

static gint
get_focus_size(GtkWidget *widget)
{
    gint focus_width, focus_pad;

    gtk_widget_style_get(widget,
                         "focus-line-width", &focus_width,
                         "focus-padding", &focus_pad,
                         NULL);

    return focus_width + focus_pad;
}

static void
get_preferred_width(GtkWidget *widget,
                    gint *minimum, gint *natural)
{
    gint fs = get_focus_size(widget);
    *minimum = 2*fs + 19;
    *natural = 2*fs + 225;
}

static void
get_preferred_height(GtkWidget *widget,
                     gint *minimum, gint *natural)
{
    gint fs = get_focus_size(widget);
    *minimum = 2*fs + 19;
    *natural = 2*fs + 225;
}

static void
size_allocate(GtkWidget *widget, GdkRectangle *allocation)
{
    GwyColorWheel *wheel = GWY_COLOR_WHEEL(widget);
    GwyColorWheelPrivate *priv = wheel->priv;

    parent_class->size_allocate(widget, allocation);

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

    gint fs = get_focus_size(widget);
    gint d = MIN(allocation->width, allocation->height) - 2*fs;
    d = MAX(3, d);
    priv->diameter = d;
    priv->inner_diameter = MIN(d-2, GWY_ROUND(0.8*d));

    g_clear_object(&priv->wheel_pixbuf);
    priv->wheel_pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, FALSE, 8, d, d);
}

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 inline gdouble
wheel_xy_to_hue(gdouble x, gdouble y)
{
    gdouble phi = atan2(-y, x); /* Range -π..π */
    return fmod(0.5*phi/G_PI + 1.0, 1.0);
}

static inline gdouble
hue_to_phi(gdouble h)
{
    /* Angle is periodic so we do not care to which period it falls, unlike for hue which we force to [0..1]. */
    return -2.0*G_PI*h;
}

/*                      ● Full colour
 *                    _-|
 *         s=1 line _-  |
 *                _-    |
 * Singularity  _-      |
 * undefined   ●        | v=1 line
 * saturation   -_      |
 * (Black)        -_    |
 *                  -_  |
 *         s=0 line   -_|
 *         (Grey)       ● White
 *
 * If we put the origin to the singularity then in these coordinates we simply have v=x, s=y/x+1/2. However, the
 * anchor at the big circle is not black but the full colour, which we imagine on the right for h=0. So we need to
 * rotate by additional π/3. */
static inline void
triangle_hsv_to_xy(gdouble h, gdouble s, gdouble v,
                   gdouble *x, gdouble *y)
{
    gdouble phi = hue_to_phi(h);
    gdouble cphi = cos(phi + G_PI/3.0), sphi = sin(phi + G_PI/3.0);
    gdouble xprime = 1.5*v - 1;
    gdouble yprime = GWY_SQRT3*v*(s - 0.5);
    *x = xprime*cphi + yprime*sphi;
    *y = xprime*sphi - yprime*cphi;
}

static inline void
triangle_hxy_to_sv(gdouble h, gdouble x, gdouble y,
                   gdouble *s, gdouble *v)
{
    gdouble phi = hue_to_phi(h);
    gdouble cphi = cos(phi + G_PI/3.0), sphi = sin(phi + G_PI/3.0);
    gdouble xprime = x*cphi + y*sphi;
    gdouble yprime = x*sphi - y*cphi;
    /* NB: These may be outside [0..1]. The caller has to check/clamp them. */
    *s = xprime + 1 == 0 ? 0.5 : GWY_SQRT3*yprime/(xprime + 1)/2 + 0.5;
    *v = 2*(xprime + 1)/3;
}

/* We only do this on resize so it is probably OK to use slow maths functions. */
static void
paint_wheel(GwyColorWheel *wheel)
{
    GwyColorWheelPrivate *priv = wheel->priv;

    guchar *pixels = gdk_pixbuf_get_pixels(priv->wheel_pixbuf);
    gint rowstride = gdk_pixbuf_get_rowstride(priv->wheel_pixbuf);
    gint d = priv->diameter;
    gdouble q = 2.0/(d - 1.0), off = -1.0;

    for (gint i = 0; i < d; i++) {
        guchar *row = pixels + i*rowstride;
        gdouble y = q*i + off;
        for (gint j = 0; j < d; j++) {
            gdouble x = q*j + off;
            gdouble h = wheel_xy_to_hue(x, y);
            gdouble r, g, b;
            gtk_hsv_to_rgb(h, 1.0, 1.0, &r, &g, &b);
            row[3*j] = float_to_hex(r);
            row[3*j+1] = float_to_hex(g);
            row[3*j+2] = float_to_hex(b);
        }
    }
}

static gboolean
draw(GtkWidget *widget, cairo_t *cr)
{
    paint_wheel(GWY_COLOR_WHEEL(widget));

    GwyColorWheelPrivate *priv = GWY_COLOR_WHEEL(widget)->priv;
    GdkRectangle allocation;
    gtk_widget_get_allocation(widget, &allocation);

    GtkStateFlags state = gtk_widget_get_state_flags(widget);
    gboolean is_insensitive = state & GTK_STATE_FLAG_INSENSITIVE;
    gint d = priv->diameter, di = priv->inner_diameter;
    gdouble cx = 0.5*allocation.width, cy = 0.5*allocation.height;
    GwyRGBA *rgba = &priv->color;
    gdouble phi = hue_to_phi(priv->h);
    gdouble cphi = cos(phi), sphi = sin(phi);
    gdouble r, g, b;

    gtk_hsv_to_rgb(priv->h, 1.0, 1.0, &r, &g, &b);

    /* The triangle, as a gradient mesh.
     * FIXME: Drawing a single piece of gradient mesh is nice and simple, but the triangle edges are then not aliased.
     * Not sure how to improve it. */
    cairo_save(cr);
    cairo_pattern_t *pattern = cairo_pattern_create_mesh();
    cairo_mesh_pattern_begin_patch(pattern);
    cairo_mesh_pattern_move_to(pattern, 0.5*di*cphi, 0.5*di*sphi);
    cairo_mesh_pattern_line_to(pattern, 0.5*di*cos(phi + 2.0*G_PI/3.0), 0.5*di*sin(phi + 2.0*G_PI/3.0));
    cairo_mesh_pattern_line_to(pattern, 0.5*di*cos(phi - 2.0*G_PI/3.0), 0.5*di*sin(phi - 2.0*G_PI/3.0));
    cairo_mesh_pattern_line_to(pattern, 0.5*di*cphi, 0.5*di*sphi);
    cairo_mesh_pattern_line_to(pattern, 0.5*di*cphi, 0.5*di*sphi);
    cairo_mesh_pattern_set_corner_color_rgb(pattern, 0, r, g, b);
    cairo_mesh_pattern_set_corner_color_rgb(pattern, 3, r, g, b);
    gtk_hsv_to_rgb(priv->h, 0.0, 1.0, &r, &g, &b);
    cairo_mesh_pattern_set_corner_color_rgb(pattern, 1, r, g, b);
    gtk_hsv_to_rgb(priv->h, 0.0, 0.0, &r, &g, &b);
    cairo_mesh_pattern_set_corner_color_rgb(pattern, 2, r, g, b);
    cairo_mesh_pattern_end_patch(pattern);
    cairo_translate(cr, cx, cy);
    if (is_insensitive)
        cairo_push_group(cr);
    cairo_set_source(cr, pattern);
    cairo_paint(cr);
    if (is_insensitive) {
        cairo_pop_group_to_source(cr);
        cairo_paint_with_alpha(cr, 0.6);
    }
    cairo_pattern_destroy(pattern);
    cairo_restore(cr);

    /* The wheel. It is a fixed pixbuf, just clip it. */
    cairo_save(cr);
    cairo_new_sub_path(cr);
    cairo_arc(cr, cx, cy, 0.5*d, 0.0, 2.0*G_PI);
    cairo_line_to(cr, cx + 0.5*di, cy);
    cairo_arc_negative(cr, cx, cy, 0.5*di, 2.0*G_PI, 0.0);
    cairo_close_path(cr);
    cairo_translate(cr, cx - 0.5*d, cy - 0.5*d);
    cairo_clip_preserve(cr);
    if (is_insensitive)
        cairo_push_group(cr);
    gdk_cairo_set_source_pixbuf(cr, priv->wheel_pixbuf, 0, 0);
    cairo_paint(cr);

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

    /* Selected hue on the wheel. */
    cairo_save(cr);
    if (is_color_bright(r, g, b))
        cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
    else
        cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
    cairo_set_line_width(cr, 1.0 + 0.003*d);
    cairo_new_path(cr);
    cairo_move_to(cr, cx + 0.5*di*cphi, cy + 0.5*di*sphi);
    cairo_line_to(cr, cx + 0.5*d*cphi, cy + 0.5*d*sphi);
    if (is_insensitive)
        cairo_push_group(cr);
    cairo_stroke(cr);
    if (is_insensitive) {
        cairo_pop_group_to_source(cr);
        cairo_paint_with_alpha(cr, 0.6);
    }
    cairo_restore(cr);

    /* Selected saturation and value on the triangle. */
    gdouble x, y;
    triangle_hsv_to_xy(priv->h, priv->s, priv->v, &x, &y);
    cairo_save(cr);
    if (is_color_bright(rgba->r, rgba->g, rgba->b))
        cairo_set_source_rgb(cr, 0.0, 0.0, 0.0);
    else
        cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
    cairo_set_line_width(cr, 1.0 + 0.003*d);
    cairo_new_path(cr);
    cairo_arc(cr, cx + x*0.5*di, cy + y*0.5*di, 1.0 + 0.01*d, 0.0, 2.0*G_PI);
    cairo_close_path(cr);
    if (is_insensitive)
        cairo_push_group(cr);
    cairo_stroke(cr);
    if (is_insensitive) {
        cairo_pop_group_to_source(cr);
        cairo_paint_with_alpha(cr, 0.6);
    }
    cairo_restore(cr);

    if (gtk_widget_has_focus(widget))
        gtk_render_focus(gtk_widget_get_style_context(widget), cr, 0, 0, allocation.width, allocation.height);

    return FALSE;
}

static void
shift_xy_to_centre(GtkWidget *widget, gdouble *x, gdouble *y)
{
    GdkRectangle allocation;
    gtk_widget_get_allocation(widget, &allocation);

    *x -= 0.5*allocation.width;
    *y -= 0.5*allocation.height;
}

static gboolean
button_pressed(GtkWidget *widget, GdkEventButton *event)
{
    GwyColorWheel *wheel = GWY_COLOR_WHEEL(widget);
    GwyColorWheelPrivate *priv = wheel->priv;
    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);

    gdouble x = event->x, y = event->y;
    shift_xy_to_centre(widget, &x, &y);
    gdouble d = priv->diameter;
    gdouble di = priv->inner_diameter;
    gdouble r2 = x*x + y*y;
    ColorWheelPiece editing = PIECE_NONE;

    if (r2 <= 0.25*d*d && r2 >= 0.25*di*di) {
        editing = PIECE_WHEEL;
        update_hue(wheel, x, y);
    }
    else if (r2 <= 0.25*di*di && update_saturation_value(wheel, x, y, TRUE)) {
        editing = PIECE_TRIANGLE;
    }

    if (editing != PIECE_NONE) {
        priv->editing = editing;
        gtk_grab_add(widget);
        priv->button = button;
    }
    return TRUE;
}

static gboolean
button_released(GtkWidget *widget, GdkEventButton *event)
{
    GwyColorWheel *wheel = GWY_COLOR_WHEEL(widget);
    GwyColorWheelPrivate *priv = wheel->priv;
    gint button = event->button;

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

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

    gdouble x = event->x, y = event->y;
    shift_xy_to_centre(widget, &x, &y);
    if (priv->editing == PIECE_WHEEL)
        update_hue(wheel, x, y);
    else if (priv->editing == PIECE_TRIANGLE)
        update_saturation_value(wheel, x, y, FALSE);
    return TRUE;
}

static gboolean
pointer_moved(GtkWidget *widget, GdkEventMotion *event)
{
    GwyColorWheel *wheel = GWY_COLOR_WHEEL(widget);
    GwyColorWheelPrivate *priv = wheel->priv;

    if (!priv->button)
        return FALSE;

    gdouble x = event->x, y = event->y;
    shift_xy_to_centre(widget, &x, &y);

    if (priv->editing == PIECE_WHEEL)
        update_hue(wheel, x, y);
    else if (priv->editing == PIECE_TRIANGLE)
        update_saturation_value(wheel, x, y, FALSE);
    return TRUE;
}

static gboolean
key_pressed(GtkWidget *widget, GdkEventKey *event)
{
    GwyColorWheel *wheel = GWY_COLOR_WHEEL(widget);
    GwyColorWheelPrivate *priv = wheel->priv;

    guint keyval = event->keyval;
    if (priv->editing == PIECE_WHEEL) {
        gdouble h = priv->h, step = 0.005;

        /* Scroll spatially. GIMP simply scrolls to larger hue with up and right, even though it means moving in the
         * opposite spatial direction on the wheel. It is confusing. This should be less confusing, although possibly
         * more annoying as the movement can stop. */
        if (keyval == GDK_KEY_Up || keyval == GDK_KEY_KP_Up) {
            if (h >= 0.75 || h < 0.25) {
                h = fmod(h + step, 1.0);
                if (h < 0.5)
                    h = fmin(h, 0.25);
            }
            else if (h > 0.25 && h < 0.75)
                h = fmax(h - step, 0.25);
        }
        else if (keyval == GDK_KEY_Down || keyval == GDK_KEY_KP_Down) {
            if (h > 0.75 || h < 0.25) {
                h = fmod(h + 1.0 - step, 1.0);
                if (h > 0.5)
                    h = fmax(h, 0.75);
            }
            else if (h >= 0.25 && h < 0.75)
                h = fmin(h + step, 0.75);
        }
        else if (keyval == GDK_KEY_Left || keyval == GDK_KEY_KP_Left) {
            if (h >= 0.0 && h < 0.5)
                h = fmin(h + step, 0.5);
            else if (h > 0.5)
                h = fmax(h - step, 0.5);
        }
        else if (keyval == GDK_KEY_Right || keyval == GDK_KEY_KP_Right) {
            if (h > 0.0 && h <= 0.5)
                h = fmax(h - step, 0.0);
            else if (h > 0.5)
                h = fmin(h + step, 1.0);
        }
        else
            return FALSE;

        if (priv->h != h) {
            priv->h = h;
            update_rgb_from_hsv(wheel);
        }
        return TRUE;
    }
    else if (priv->editing == PIECE_TRIANGLE) {
        gdouble h = priv->h, s = priv->s, v = priv->v, x, y, step = 0.0125;
        triangle_hsv_to_xy(h, s, v, &x, &y);
        if (keyval == GDK_KEY_Up || keyval == GDK_KEY_KP_Up)
            y -= step;
        else if (keyval == GDK_KEY_Down || keyval == GDK_KEY_KP_Down)
            y += step;
        else if (keyval == GDK_KEY_Left || keyval == GDK_KEY_KP_Left)
            x -= step;
        else if (keyval == GDK_KEY_Right || keyval == GDK_KEY_KP_Right)
            x += step;
        else
            return FALSE;

        gdouble di = priv->inner_diameter;
        update_saturation_value(wheel, x*0.5*di, y*0.5*di, FALSE);
        return TRUE;
    }

    return FALSE;
}

static gboolean
scrolled(GtkWidget *widget, GdkEventScroll *event)
{
    GwyColorWheel *wheel = GWY_COLOR_WHEEL(widget);
    GwyColorWheelPrivate *priv = wheel->priv;
    gdouble h = priv->h, step = 0.005;
    priv->h = fmod(h + 1.0 - step*event->delta_y, 1.0);
    update_rgb_from_hsv(wheel);
    return TRUE;
}

static void
update_hue(GwyColorWheel *wheel, gdouble x, gdouble y)
{
    GwyColorWheelPrivate *priv = wheel->priv;

    gdouble h = wheel_xy_to_hue(x, y);
    if (priv->h != h) {
        priv->h = h;
        update_rgb_from_hsv(wheel);
    }
}

/* FIXME: This does not require redrawing everything because we are only moving a marker. */
static gboolean
update_saturation_value(GwyColorWheel *wheel, gdouble x, gdouble y, gboolean only_inside)
{
    GwyColorWheelPrivate *priv = wheel->priv;
    gdouble di = priv->inner_diameter;

    gdouble s, v;
    triangle_hxy_to_sv(priv->h, x/(0.5*di), y/(0.5*di), &s, &v);
    gdouble good_v = fmin(fmax(v, 0.0), 1.0);
    gdouble good_s = fmin(fmax(s, 0.0), 1.0);
    if (only_inside && (good_s != s || good_v != v))
        return FALSE;

    if (priv->s != good_s || priv->v != good_v) {
        priv->s = good_s;
        priv->v = good_v;
        update_rgb_from_hsv(wheel);
    }
    return TRUE;
}

static void
update_rgb_from_hsv(GwyColorWheel *wheel)
{
    GwyColorWheelPrivate *priv = wheel->priv;
    GwyRGBA *rgba = &priv->color;
    gtk_hsv_to_rgb(priv->h, priv->s, priv->v, &rgba->r, &rgba->g, &rgba->b);
    if (gtk_widget_is_drawable(GTK_WIDGET(wheel)))
        gtk_widget_queue_draw(GTK_WIDGET(wheel));
    g_signal_emit(wheel, signals[SGNL_COLOR_CHANGED], 0);
}

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;
}

static void
state_flags_changed(GtkWidget *widget, GtkStateFlags old_state)
{
    if ((gtk_widget_get_state_flags(widget) ^ old_state) & GTK_STATE_FLAG_INSENSITIVE)
        gtk_widget_queue_draw(widget);

    parent_class->state_flags_changed(widget, old_state);
}

/**
 * SECTION: color-wheel
 * @title: GwyColorWheel
 * @short_description: Colour editing component
 *
 * #GwyColorWheel is an HSV colour selector consiting of a hue ring with a saturation/value triangle inside.
 **/

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