/*
 *  $Id: shader.c 28670 2025-10-20 13:45:15Z yeti-dn $
 *  Copyright (C) 2003-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 <glib-object.h>
#include <gdk/gdkkeysyms.h>
#include <gdk-pixbuf/gdk-pixbuf.h>
#include <gtk/gtk.h>

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

#include "libgwyui/shader.h"
#include "libgwyui/widget-impl-utils.h"

enum {
    NSAMPLES = 2048
};

enum {
    PROP_0,
    PROP_GRADIENT,
    NUM_PROPERTIES
};

enum {
    SGNL_ANGLE_CHANGED,
    NUM_SIGNALS
};

struct _GwyShaderPrivate {
    GdkWindow *event_window;

    GwyGradient *gradient;
    guchar *gradient_samples;
    gulong gradient_changed_id;
    gulong default_gradient_changed_id;
    gboolean samples_valid;

    guint8 button;
    gint diameter;

    gdouble theta;
    gdouble phi;

    /* The sphere */
    GwyXYZ *xyz;
    GdkPixbuf *pixbuf;
};

static void     finalize                (GObject *object);
static void     dispose                 (GObject *object);
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     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     fill_xyz                (GwyShader *shader);
static void     render_sphere           (GwyShader *shader);
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 motion_notify           (GtkWidget *widget,
                                         GdkEventMotion *event);
static gboolean key_pressed             (GtkWidget *widget,
                                         GdkEventKey *event);
static void     update_angles           (GwyShader *shader,
                                         gdouble x,
                                         gdouble y);
static gboolean mnemonic_activate       (GtkWidget *widget,
                                         gboolean group_cycling);
static void     state_flags_changed     (GtkWidget *widget,
                                         GtkStateFlags old_state);
static void     gradient_changed        (GwyShader *shader);
static void     follow_default_gradient (GwyShader *shader,
                                         gboolean follow);
static void     default_gradient_changed(GwyShader *shader);

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

G_DEFINE_TYPE_WITH_CODE(GwyShader, gwy_shader, GTK_TYPE_WIDGET,
                        G_ADD_PRIVATE(GwyShader))

static void
gwy_shader_class_init(GwyShaderClass *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_shader_parent_class;

    gobject_class->finalize = finalize;
    gobject_class->dispose = dispose;
    gobject_class->set_property = set_property;
    gobject_class->get_property = get_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 = motion_notify;
    widget_class->key_press_event = key_pressed;
    widget_class->mnemonic_activate = mnemonic_activate;
    widget_class->state_flags_changed = state_flags_changed;

    /**
     * GwyShader::angle-changed:
     * @gwyshader: The #GwyShader which received the signal.
     *
     * The ::angle-changed signal is emitted when the spherical angle changes.
     **/
    signals[SGNL_ANGLE_CHANGED] = g_signal_new("angle-changed", type,
                                               G_SIGNAL_RUN_FIRST,
                                               G_STRUCT_OFFSET(GwyShaderClass, angle_changed),
                                               NULL, NULL,
                                               g_cclosure_marshal_VOID__VOID,
                                               G_TYPE_NONE, 0);
    g_signal_set_va_marshaller(signals[SGNL_ANGLE_CHANGED], type, g_cclosure_marshal_VOID__VOIDv);

    properties[PROP_GRADIENT] = g_param_spec_object("gradient", NULL,
                                                    "Gradient to use for rendering",
                                                    GWY_TYPE_GRADIENT, GWY_GPARAM_RWE);

    g_object_class_install_properties(gobject_class, NUM_PROPERTIES, properties);
}

static void
gwy_shader_init(GwyShader *shader)
{
    G_GNUC_UNUSED GwyShaderPrivate *priv;

    priv = shader->priv = gwy_shader_get_instance_private(shader);

    gwy_shader_set_gradient(shader, NULL);
    gtk_widget_set_has_window(GTK_WIDGET(shader), FALSE);
    gtk_widget_set_can_focus(GTK_WIDGET(shader), TRUE);
}

/**
 * gwy_shader_new:
 *
 * Creates a new spherical shader.
 *
 * The widget takes up all the space allocated for it.
 *
 * Returns: The new shader widget.
 **/
GtkWidget*
gwy_shader_new(void)
{
    return gtk_widget_new(GWY_TYPE_SHADER, NULL);
}

static void
finalize(GObject *object)
{
    GwyShaderPrivate *priv = GWY_SHADER(object)->priv;

    g_clear_object(&priv->pixbuf);
    g_clear_object(&priv->gradient);
    GWY_FREE(priv->xyz);
    GWY_FREE(priv->gradient_samples);

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

static void
dispose(GObject *object)
{
    GwyShader *shader = GWY_SHADER(object);
    GwyShaderPrivate *priv = shader->priv;

    follow_default_gradient(shader, FALSE);
    g_clear_signal_handler(&priv->gradient_changed_id, priv->gradient);

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

static void
realize(GtkWidget *widget)
{
    GwyShader *shader = GWY_SHADER(widget);
    GwyShaderPrivate *priv = shader->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);
}

static void
unrealize(GtkWidget *widget)
{
    GwyShaderPrivate *priv = GWY_SHADER(widget)->priv;

    g_clear_object(&priv->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)
{
    GwyShaderPrivate *priv = GWY_SHADER(widget)->priv;
    GTK_WIDGET_CLASS(parent_class)->map(widget);
    gdk_window_show(priv->event_window);
}

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

static void
set_property(GObject *object,
             guint prop_id,
             const GValue *value,
             GParamSpec *pspec)
{
    GwyShader *shader = GWY_SHADER(object);

    switch (prop_id) {
        case PROP_GRADIENT:
        gwy_shader_set_gradient(shader, g_value_get_object(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)
{
    GwyShader *shader = GWY_SHADER(object);
    //GwyShaderPrivate *priv = shader->priv;

    switch (prop_id) {
        case PROP_GRADIENT:
        g_value_set_object(value, gwy_shader_get_gradient(shader));
        break;

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

/**
 * gwy_shader_get_theta:
 * @shader: A shaded spherical angle selector.
 *
 * Returns the theta coordinate of a shaded spherical angle selector.
 *
 * Returns: The theta coordinate, in radians.  Theta coordinate is angle from sphere's north pole.
 **/
gdouble
gwy_shader_get_theta(GwyShader *shader)
{
    g_return_val_if_fail(GWY_IS_SHADER(shader), 0.0);
    return shader->priv->theta;
}

/**
 * gwy_shader_get_phi:
 * @shader: A shaded spherical angle selector.
 *
 * Returns the phi coordinate of a shaded spherical angle selector.
 *
 * Returns: The phi coordinate, in radians.  Phi coordinate is orientation in horizontal plane, measured from @x axis,
 *          counterclockwise.
 **/
gdouble
gwy_shader_get_phi(GwyShader *shader)
{
    g_return_val_if_fail(GWY_IS_SHADER(shader), 0.0);
    return shader->priv->phi;
}

/**
 * gwy_shader_get_gradient:
 * @shader: A shaded spherical angle selector.
 *
 * Returns the color gradient a shaded spherical angle selector uses.
 *
 * If no specific gradient has been set and the default one is used, the function returns %NULL. This is consistent
 * with getting the corresponding property. If you still need the #GwyGradient object in such case, use
 * gwy_gradients_get_gradient() with %NULL argument.
 *
 * Returns: (transfer none) (nullable):
 *          The current colour gradient, possibly %NULL.
 **/
GwyGradient*
gwy_shader_get_gradient(GwyShader *shader)
{
    g_return_val_if_fail(GWY_IS_SHADER(shader), NULL);
    GwyShaderPrivate *priv = shader->priv;
    return priv->default_gradient_changed_id ? NULL : priv->gradient;
}

/**
 * gwy_shader_set_theta:
 * @shader: A shaded spherical angle selector.
 * @theta: The theta coordinate to set.  See gwy_shader_get_theta() for description.
 *
 * Sets the theta coordinate of a shaded spherical angle selector.
 **/
void
gwy_shader_set_theta(GwyShader *shader,
                     gdouble theta)
{
    g_return_if_fail(GWY_IS_SHADER(shader));

    GwyShaderPrivate *priv = shader->priv;
    theta = CLAMP(theta, 0.0, G_PI/2);
    if (theta == priv->theta)
        return;

    priv->theta = theta;
    g_signal_emit(shader, signals[SGNL_ANGLE_CHANGED], 0);
    gtk_widget_queue_draw(GTK_WIDGET(shader));
}

/**
 * gwy_shader_set_phi:
 * @shader: A shaded spherical angle selector.
 * @phi: The phi coordinate to set.  See gwy_shader_get_phi() for a description.
 *
 * Sets the phi coordinate of a shaded spherical angle selector.
 **/
void
gwy_shader_set_phi(GwyShader *shader,
                   gdouble phi)
{
    g_return_if_fail(GWY_IS_SHADER(shader));

    GwyShaderPrivate *priv = shader->priv;
    phi = gwy_canonicalize_angle(phi, TRUE, TRUE);
    if (phi == priv->phi)
        return;

    priv->phi = phi;
    g_signal_emit(shader, signals[SGNL_ANGLE_CHANGED], 0);
    gtk_widget_queue_draw(GTK_WIDGET(shader));
}

/**
 * gwy_shader_set_angle:
 * @shader: A shaded spherical angle selector.
 * @theta: The theta coordinate to set.  See gwy_shader_get_theta() for a description.
 * @phi: The phi coordinate to set.  See gwy_shader_get_phi() for a description.
 *
 * Sets both spherical angles of a shaded spherical angle selector.
 **/
void
gwy_shader_set_angle(GwyShader *shader,
                     gdouble theta,
                     gdouble phi)
{
    g_return_if_fail(GWY_IS_SHADER(shader));

    GwyShaderPrivate *priv = shader->priv;
    theta = CLAMP(theta, 0.0, G_PI/2);
    phi = gwy_canonicalize_angle(phi, TRUE, TRUE);
    if (theta == priv->theta && phi == priv->phi)
        return;

    priv->theta = theta;
    priv->phi = phi;
    g_signal_emit(shader, signals[SGNL_ANGLE_CHANGED], 0);
    gtk_widget_queue_draw(GTK_WIDGET(shader));
}

/**
 * gwy_shader_set_gradient:
 * @shader: A shaded spherical angle selector.
 * @gradient: (transfer none) (nullable):
 *            Colour gradient shader sperical angle selector should use.
 *
 * Sets the gradient a shaded spherical angle selector uses.
 **/
void
gwy_shader_set_gradient(GwyShader *shader,
                        GwyGradient *gradient)
{
    g_return_if_fail(GWY_IS_SHADER(shader));
    g_return_if_fail(!gradient || GWY_IS_GRADIENT(gradient));
    GwyShaderPrivate *priv = shader->priv;
    GwyGradient *real_gradient = gradient ? gradient : gwy_gradients_get_gradient(NULL);
    gboolean prop_changed = (priv->default_gradient_changed_id ? !!gradient : gradient != priv->gradient);
    gboolean visually_changed = gwy_set_member_object(shader, real_gradient, GWY_TYPE_GRADIENT, &priv->gradient,
                                                      "data-changed", gradient_changed,
                                                      &priv->gradient_changed_id, G_CONNECT_SWAPPED,
                                                      NULL);
    if (visually_changed) {
        priv->samples_valid = FALSE;
        gtk_widget_queue_draw(GTK_WIDGET(shader));
    }
    if (prop_changed) {
        follow_default_gradient(shader, !gradient);
        g_object_notify_by_pspec(G_OBJECT(shader), properties[PROP_GRADIENT]);
    }
}

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 + 3;
    *natural = 2*fs + 49;
}

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

static void
size_allocate(GtkWidget *widget, GdkRectangle *allocation)
{
    GwyShader *shader = GWY_SHADER(widget);
    GwyShaderPrivate *priv = shader->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(1, d);
    priv->diameter = d;

    g_clear_object(&priv->pixbuf);
    priv->pixbuf = gdk_pixbuf_new(GDK_COLORSPACE_RGB, FALSE, 8, d, d);
    priv->xyz = g_renew(GwyXYZ, priv->xyz, d*d);
    fill_xyz(shader);
}

static void
fill_xyz(GwyShader *shader)
{
    GwyShaderPrivate *priv = shader->priv;
    gint d = priv->diameter;
    GwyXYZ *xyz = priv->xyz;

    for (gint i = 0; i < d; i++) {
        gdouble y = (2*i + 1.0)/d - 1.0;
        for (gint j = 0; j < d; j++) {
            gdouble x = (2*j + 1.0)/d - 1.0;
            gdouble z = sqrt(fmax(0.0, 1.0 - x*x - y*y));
            xyz[i*d + j] = (GwyXYZ){ x, y, z };
        }
    }
}

static void
render_sphere(GwyShader *shader)
{
    GwyShaderPrivate *priv = shader->priv;

    if (!priv->samples_valid) {
        priv->gradient_samples = gwy_gradient_sample(priv->gradient, NSAMPLES, priv->gradient_samples);
        priv->samples_valid = TRUE;
    }

    gdouble sphi = sin(priv->phi);
    gdouble cphi = cos(priv->phi);
    gdouble sth = sin(priv->theta);
    gdouble cth = cos(priv->theta);
    guchar *pixels = gdk_pixbuf_get_pixels(priv->pixbuf);
    gint rowstride = gdk_pixbuf_get_rowstride(priv->pixbuf);
    gint d = priv->diameter;
    const guchar *samples = priv->gradient_samples;
    const GwyXYZ *xyz = priv->xyz;

    for (gint i = 0; i < d; i++) {
        guchar *row = pixels + i*rowstride;
        for (gint j = 0; j < d; j++) {
            gint k = i*d + j;
            gdouble u = (xyz[k].x*cphi - xyz[k].y*sphi)*sth + xyz[k].z*cth;
            gint m = GWY_ROUND(NSAMPLES*(0.5*u + 0.5));
            m = CLAMP(m, 0, NSAMPLES-1);
            row[3*j] = samples[4*m];
            row[3*j+1] = samples[4*m+1];
            row[3*j+2] = samples[4*m+2];
        }
    }
}

static gboolean
draw(GtkWidget *widget, cairo_t *cr)
{
    render_sphere(GWY_SHADER(widget));

    GwyShaderPrivate *priv = GWY_SHADER(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;
    gdouble cx = 0.5*allocation.width, cy = 0.5*allocation.height;
    cairo_save(cr);
    cairo_new_sub_path(cr);
    cairo_arc(cr, cx, cy, 0.5*d, 0.0, 2.0*G_PI);
    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->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);

    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 gboolean
button_pressed(GtkWidget *widget, GdkEventButton *event)
{
    GwyShaderPrivate *priv = GWY_SHADER(widget)->priv;

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

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

    if (!priv->button) {
        GdkRectangle allocation;
        gtk_widget_get_allocation(widget, &allocation);

        gdouble x = event->x - 0.5*allocation.width;
        gdouble y = event->y - 0.5*allocation.height;
        gdouble d = priv->diameter;
        if (x*x + y*y <= 0.25*d*d) {
            gtk_grab_add(widget);
            priv->button = event->button;
            update_angles(GWY_SHADER(widget), event->x, event->y);

            return TRUE;
        }
    }

    return FALSE;
}

static gboolean
button_released(GtkWidget *widget, GdkEventButton *event)
{
    GwyShaderPrivate *priv = GWY_SHADER(widget)->priv;

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

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

    update_angles(GWY_SHADER(widget), event->x, event->y);

    return FALSE;
}

static gboolean
motion_notify(GtkWidget *widget, GdkEventMotion *event)
{
    GwyShaderPrivate *priv = GWY_SHADER(widget)->priv;

    if (!priv->button)
        return FALSE;

    update_angles(GWY_SHADER(widget), event->x, event->y);

    return FALSE;
}

static void
update_angles(GwyShader *shader, gdouble x, gdouble y)
{
    GwyShaderPrivate *priv = shader->priv;

    GdkRectangle allocation;
    gtk_widget_get_allocation(GTK_WIDGET(shader), &allocation);

    gdouble r = 0.5*priv->diameter;
    x = (x - 0.5*allocation.width)/r;
    y = (y - 0.5*allocation.height)/r;

    gdouble h = sqrt(x*x + y*y);
    gdouble phi = atan2(-y, x);
    gdouble theta = (h >= 1.0 ? G_PI/2.0 : asin(h));
    gwy_shader_set_angle(shader, theta, phi);
}

static gboolean
key_pressed(GtkWidget *widget, GdkEventKey *event)
{
    GwyShader *shader = GWY_SHADER(widget);
    GwyShaderPrivate *priv = shader->priv;
    gint keyval = event->keyval;

    if (keyval == GDK_KEY_Up || keyval == GDK_KEY_KP_Up)
        gwy_shader_set_theta(shader, priv->theta - G_PI/48);
    else if (keyval == GDK_KEY_Down || keyval == GDK_KEY_KP_Down)
        gwy_shader_set_theta(shader, priv->theta + G_PI/48);
    else if (keyval == GDK_KEY_Left || keyval == GDK_KEY_KP_Left)
        gwy_shader_set_phi(shader, priv->phi + G_PI/48);
    else if (keyval == GDK_KEY_Right || keyval == GDK_KEY_KP_Right)
        gwy_shader_set_phi(shader, priv->phi - G_PI/48);
    else
        return FALSE;

    return TRUE;
}

static gboolean
mnemonic_activate(GtkWidget *widget, G_GNUC_UNUSED gboolean group_cycling)
{
    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);
}

static void
gradient_changed(GwyShader *shader)
{
    GtkWidget *widget = GTK_WIDGET(shader);
    GwyShaderPrivate *priv = shader->priv;
    priv->samples_valid = FALSE;
    if (gtk_widget_is_drawable(widget))
        gtk_widget_queue_draw(widget);
}

static void
follow_default_gradient(GwyShader *shader, gboolean follow)
{
    GwyShaderPrivate *priv = shader->priv;
    GwyInventory *inventory = gwy_gradients();
    if (!follow) {
        g_clear_signal_handler(&priv->default_gradient_changed_id, inventory);
        return;
    }
    if (priv->default_gradient_changed_id)
        return;
    priv->default_gradient_changed_id = g_signal_connect_swapped(inventory, "default-changed",
                                                                 G_CALLBACK(default_gradient_changed), shader);
}

static void
default_gradient_changed(GwyShader *shader)
{
    /* That should do it. */
    gwy_shader_set_gradient(shader, NULL);
}

/**
 * SECTION:shader
 * @title: GwyShader
 * @short_description: Spherical angle selector
 *
 * #GwyShader is a spherical angle selector that allows user to change angle by simply moving the north pole of
 * a sphere around with mouse.  It can display the sphere colored with a #GwyGradient.
 **/

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