/*
 *  $Id: ruler.c 28801 2025-11-05 11:52:53Z 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 <string.h>

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

#include "libgwyui/ruler.h"
#include "libgwyui/types.h"
#include "libgwyui/cairo-utils.h"
#include "libgwyui/widget-impl-utils.h"

enum {
    MINIMUM_INCR = 5,
    LINE_WIDTH = 1,
};

enum {
    PROP_0,
    PROP_LOWER,
    PROP_UPPER,
    PROP_POSITION,
    PROP_MAX_SIZE,
    PROP_UNITS_PLACEMENT,
    NUM_PROPERTIES,
    PROP_ORIENTATION = NUM_PROPERTIES,
};

typedef enum {
    SCALE_0 = 0,
    SCALE_1,
    SCALE_2,
    SCALE_2_5,
    SCALE_5,
    NUM_SCALES,
} ScaleBase;

struct _GwyRulerPrivate {
    PangoContext *context;
    PangoLayout *layout;
    gint hthickness, vthickness, height, pixelsize;
    gdouble lower;    /* The upper limit of the ruler (in physical units) */
    gdouble upper;    /* The lower limit of the ruler */
    gdouble position;    /* The position of the mark on the ruler */

    GdkWindow *event_window;
    GwyValueFormat *vformat;
    gint xsrc, ysrc;
    gdouble max_size;    /* The maximum size of the ruler */
    gdouble drawn_position;
    gboolean drawn_position_set;
    GwyUnit *unit;
    gulong unit_changed_id;
    GwyUnitsPlacement units_placement;
    GtkOrientation orientation;
    gint min_width;
    gint min_height;
};

struct _GwyRulerClassPrivate {
    gint dummy;
};

static void      dispose             (GObject *object);
static void      finalize            (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      get_preferred_width (GtkWidget *widget,
                                      gint *minimum,
                                      gint *natural);
static void      get_preferred_height(GtkWidget *widget,
                                      gint *minimum,
                                      gint *natural);
static void      realize             (GtkWidget *widget);
static void      unrealize           (GtkWidget *widget);
static void      map                 (GtkWidget *widget);
static void      unmap               (GtkWidget *widget);
static void      screen_changed      (GtkWidget *widget,
                                      GdkScreen *previous_screen);
static void      size_allocate       (GtkWidget *widget,
                                      GdkRectangle *allocation);
static gint      draw                (GtkWidget *widget,
                                      cairo_t *cr);
static gboolean  motion_notify       (GtkWidget *widget,
                                      GdkEventMotion *event);
static void      set_min_sizes       (GwyRuler *ruler);
static void      update_pango        (GwyRuler *ruler);
static void      queue_marker_redraw (GwyRuler *ruler,
                                      gdouble value);
static void      unit_changed        (GwyRuler *ruler);
static void      update_value_format (GwyRuler *ruler);
static void      draw_ticks          (GwyRuler *ruler,
                                      cairo_t *cr);
static gboolean  is_precision_ok     (const GwyValueFormat *format,
                                      gdouble base,
                                      ScaleBase scale);
static ScaleBase next_scale          (ScaleBase scale,
                                      gdouble *base,
                                      gdouble measure,
                                      gint min_incr);

static const gdouble scale_steps[NUM_SCALES] = {
    0.0, 1.0, 2.0, 2.5, 5.0,
};

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

G_DEFINE_TYPE_WITH_CODE(GwyRuler, gwy_ruler, GTK_TYPE_WIDGET,
                        G_ADD_PRIVATE(GwyRuler)
                        G_IMPLEMENT_INTERFACE(GTK_TYPE_ORIENTABLE, NULL))

static void
gwy_ruler_class_init(GwyRulerClass *class)
{
    GObjectClass *gobject_class = G_OBJECT_CLASS(class);
    GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(class);
    //GType type = G_TYPE_FROM_CLASS(klass);

    parent_class = gwy_ruler_parent_class;

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

    widget_class->motion_notify_event = motion_notify;
    widget_class->get_preferred_width = get_preferred_width;
    widget_class->get_preferred_height = get_preferred_height;
    widget_class->realize = realize;
    widget_class->unrealize = unrealize;
    widget_class->map = map;
    widget_class->unmap = unmap;
    widget_class->screen_changed = screen_changed;
    widget_class->size_allocate = size_allocate;
    widget_class->draw = draw;

    properties[PROP_LOWER] = g_param_spec_double("lower", NULL,
                                                 "Lower limit (minimum value)",
                                                 -G_MAXDOUBLE, G_MAXDOUBLE, 0.0,
                                                 GWY_GPARAM_RWE);
    properties[PROP_UPPER] = g_param_spec_double("upper", NULL,
                                                 "Upper limit (maximum value)",
                                                 -G_MAXDOUBLE, G_MAXDOUBLE, 0.0,
                                                 GWY_GPARAM_RWE);
    properties[PROP_POSITION] = g_param_spec_double("position", NULL,
                                                    "Value where a mark should be drawn",
                                                    -G_MAXDOUBLE, G_MAXDOUBLE, 0.0,
                                                    GWY_GPARAM_RWE);
    properties[PROP_MAX_SIZE] = g_param_spec_double("max-size", NULL,
                                                    "Maximum magnigude of values",
                                                    -G_MAXDOUBLE, G_MAXDOUBLE, 0.0,
                                                    GWY_GPARAM_RWE);
    properties[PROP_UNITS_PLACEMENT] = g_param_spec_enum("units-placement", NULL,
                                                         "Placement of units",
                                                         GWY_TYPE_UNITS_PLACEMENT, GWY_UNITS_PLACEMENT_NONE,
                                                         GWY_GPARAM_RWE);

    g_object_class_install_properties(gobject_class, NUM_PROPERTIES, properties);

    g_object_class_override_property(gobject_class, PROP_ORIENTATION, "orientation");
}

static void
gwy_ruler_init(GwyRuler *ruler)
{
    GwyRulerPrivate *priv;

    ruler->priv = priv = gwy_ruler_get_instance_private(ruler);
    priv->unit = gwy_unit_new(NULL);
    priv->unit_changed_id = g_signal_connect_swapped(priv->unit, "value-changed", G_CALLBACK(unit_changed), ruler);
    priv->pixelsize = 1;
    priv->orientation = GTK_ORIENTATION_HORIZONTAL;
    set_min_sizes(ruler);
    update_value_format(ruler);

    gtk_widget_set_has_window(GTK_WIDGET(ruler), FALSE);
}

static void
dispose(GObject *object)
{
    GwyRulerPrivate *priv = GWY_RULER(object)->priv;

    g_clear_signal_handler(&priv->unit_changed_id, priv->unit);

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

static void
finalize(GObject *object)
{
    GwyRulerPrivate *priv = GWY_RULER(object)->priv;

    g_clear_object(&priv->unit);
    gwy_value_format_free(priv->vformat);

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

static void
set_property(GObject *object,
             guint prop_id,
             const GValue *value,
             GParamSpec *pspec)
{
    GwyRuler *ruler = GWY_RULER(object);
    GwyRulerPrivate *priv = ruler->priv;

    switch (prop_id) {
        case PROP_LOWER:
        gwy_ruler_set_range(ruler, g_value_get_double(value), priv->upper, priv->position, priv->max_size);
        break;

        case PROP_UPPER:
        gwy_ruler_set_range(ruler, priv->lower, g_value_get_double(value), priv->position, priv->max_size);
        break;

        case PROP_POSITION:
        gwy_ruler_set_range(ruler, priv->lower, priv->upper, g_value_get_double(value), priv->max_size);
        break;

        case PROP_MAX_SIZE:
        gwy_ruler_set_range(ruler, priv->lower, priv->upper, priv->position, g_value_get_double(value));
        break;

        case PROP_UNITS_PLACEMENT:
        gwy_ruler_set_units_placement(ruler, g_value_get_enum(value));
        break;

        case PROP_ORIENTATION:
        gwy_ruler_set_orientation(ruler, g_value_get_enum(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)
{
    GwyRulerPrivate *priv = GWY_RULER(object)->priv;

    switch (prop_id) {
        case PROP_LOWER:
        g_value_set_double(value, priv->lower);
        break;

        case PROP_UPPER:
        g_value_set_double(value, priv->upper);
        break;

        case PROP_POSITION:
        g_value_set_double(value, priv->position);
        break;

        case PROP_MAX_SIZE:
        g_value_set_double(value, priv->max_size);
        break;

        case PROP_UNITS_PLACEMENT:
        g_value_set_enum(value, priv->units_placement);
        break;

        case PROP_ORIENTATION:
        g_value_set_enum(value, priv->orientation);
        break;

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

static void
get_preferred_width(GtkWidget *widget,
                    gint *minimum, gint *natural)
{
    GwyRuler *ruler = GWY_RULER(widget);
    GwyRulerPrivate *priv = ruler->priv;

    *minimum = *natural = priv->min_width;
}

static void
get_preferred_height(GtkWidget *widget,
                     gint *minimum, gint *natural)
{
    GwyRuler *ruler = GWY_RULER(widget);
    GwyRulerPrivate *priv = ruler->priv;

    *minimum = *natural = priv->min_height;
}

static gboolean
motion_notify(GtkWidget *widget, GdkEventMotion *event)
{
    GwyRuler *ruler = GWY_RULER(widget);
    if (ruler->priv->orientation == GTK_ORIENTATION_HORIZONTAL)
        gwy_ruler_move_marker(ruler, gwy_ruler_coord_widget_to_real(ruler, event->x));
    else
        gwy_ruler_move_marker(ruler, gwy_ruler_coord_widget_to_real(ruler, event->y));
    gdk_event_request_motions(event);

    return FALSE;
}

/**
 * gwy_ruler_new: (constructor)
 * @orientation: Ruler orientation.
 *
 * Creates a new ruler widget.
 *
 * Returns: (transfer full):
 *          A newly created ruler widget.
 **/
GtkWidget*
gwy_ruler_new(GtkOrientation orientation)
{
    GtkWidget *widget = gtk_widget_new(GWY_TYPE_RULER, NULL);
    g_return_val_if_fail(orientation == GTK_ORIENTATION_HORIZONTAL || orientation == GTK_ORIENTATION_VERTICAL, widget);
    GwyRuler *ruler = GWY_RULER(widget);
    ruler->priv->orientation = orientation;
    set_min_sizes(ruler);
    return widget;
}

/**
 * gwy_ruler_set_orientation:
 * @ruler: A #GwyRuler.
 * @orientation: New ruler orientation.
 *
 * Sets the orientation of a ruler.
 **/
void
gwy_ruler_set_orientation(GwyRuler *ruler,
                          GtkOrientation orientation)
{
    g_return_if_fail(GWY_IS_RULER(ruler));
    g_return_if_fail(orientation == GTK_ORIENTATION_HORIZONTAL || orientation == GTK_ORIENTATION_VERTICAL);

    GwyRulerPrivate *priv = ruler->priv;
    if (priv->orientation == orientation)
        return;

    priv->orientation = orientation;
    set_min_sizes(ruler);
    update_pango(ruler);
    gtk_widget_queue_resize(GTK_WIDGET(ruler));
}

/**
 * gwy_ruler_get_orientation:
 * @ruler: A #GwyRuler.
 *
 * Gets the orientation of a ruler.
 *
 * Returns: The orientation.
 **/
GtkOrientation
gwy_ruler_get_orientation(GwyRuler *ruler)
{
    g_return_val_if_fail(GWY_IS_RULER(ruler), GTK_ORIENTATION_HORIZONTAL);
    return ruler->priv->orientation;
}

/**
 * gwy_ruler_set_range:
 * @ruler: A #GwyRuler
 * @lower: Lower limit of the ruler.
 * @upper: Upper limit of the ruler.
 * @position: Current position of the mark on the ruler.
 * @max_size: Maximum value used for calculating size of text labels.
 *
 * Sets range and current value of a ruler.
 **/
void
gwy_ruler_set_range(GwyRuler *ruler,
                    gdouble lower,
                    gdouble upper,
                    gdouble position,
                    gdouble max_size)
{
    g_return_if_fail(GWY_IS_RULER(ruler));

    GwyRulerPrivate *priv = ruler->priv;
    GObject *object = G_OBJECT(ruler);

    g_object_freeze_notify(object);
    if (priv->lower != lower) {
        priv->lower = lower;
        g_object_notify_by_pspec(object, properties[PROP_LOWER]);
    }
    if (priv->upper != upper) {
        priv->upper = upper;
        g_object_notify_by_pspec(object, properties[PROP_UPPER]);
    }
    if (priv->position != position) {
        priv->position = position;
        g_object_notify_by_pspec(object, properties[PROP_POSITION]);
    }
    if (priv->max_size != max_size) {
        priv->max_size = max_size;
        g_object_notify_by_pspec(object, properties[PROP_MAX_SIZE]);
    }
    g_object_thaw_notify(object);
    update_value_format(ruler);

    if (gtk_widget_is_drawable(GTK_WIDGET(ruler)))
        gtk_widget_queue_draw(GTK_WIDGET(ruler));
}

/**
 * gwy_ruler_get_range:
 * @ruler: A #GwyRuler
 * @lower: Location to store lower limit of the ruler, or %NULL
 * @upper: Location to store upper limit of the ruler, or %NULL
 * @position: Location to store the current position of the mark on the ruler, or %NULL
 * @max_size: Location to store the maximum size of the ruler used when calculating the space to leave for the text,
 *            or %NULL.
 *
 * Retrieves values indicating the range and current position of a #GwyRuler. See gwy_ruler_set_range().
 **/
void
gwy_ruler_get_range(GwyRuler *ruler,
                    gdouble *lower,
                    gdouble *upper,
                    gdouble *position,
                    gdouble *max_size)
{
    g_return_if_fail(GWY_IS_RULER(ruler));

    GwyRulerPrivate *priv = ruler->priv;
    if (lower)
        *lower = priv->lower;
    if (upper)
        *upper = priv->upper;
    if (position)
        *position = priv->position;
    if (max_size)
        *max_size = priv->max_size;
}

/**
 * gwy_ruler_set_units_placement:
 * @ruler: A #GwyRuler
 * @placement: Units placement specification.
 *
 * Sets whether and where units should be placed on the ruler.
 **/
void
gwy_ruler_set_units_placement(GwyRuler *ruler,
                              GwyUnitsPlacement placement)
{
    g_return_if_fail(GWY_IS_RULER(ruler));

    GwyRulerPrivate *priv = ruler->priv;
    placement = MIN(placement, GWY_UNITS_PLACEMENT_AT_ZERO);
    if (priv->units_placement == placement)
        return;

    priv->units_placement = placement;
    g_object_notify_by_pspec(G_OBJECT(ruler), properties[PROP_UNITS_PLACEMENT]);

    if (gtk_widget_is_drawable(GTK_WIDGET(ruler)))
        gtk_widget_queue_draw(GTK_WIDGET(ruler));
}

/**
 * gwy_ruler_get_units_placement:
 * @ruler: A #GwyRuler
 *
 * Gets current units placement of ruler @ruler.
 *
 * Returns: The units placement.
 **/
GwyUnitsPlacement
gwy_ruler_get_units_placement(GwyRuler *ruler)
{
    g_return_val_if_fail(GWY_IS_RULER(ruler), GWY_UNITS_PLACEMENT_NONE);
    return ruler->priv->units_placement;
}

static void
realize(GtkWidget *widget)
{
    GwyRuler *ruler = GWY_RULER(widget);
    GwyRulerPrivate *priv = ruler->priv;

    /* FIXME GTK3 widgets generally do not call parent's realize. Not sure why because it does a couple of things for
     * us like setting the widget realized. */
    GTK_WIDGET_CLASS(parent_class)->realize(widget);
    priv->event_window = gwy_create_widget_input_window(widget, GDK_POINTER_MOTION_MASK);
    update_pango(ruler);
}

static void
unrealize(GtkWidget *widget)
{
    GwyRuler *ruler = GWY_RULER(widget);
    GwyRulerPrivate *priv = ruler->priv;

    gwy_destroy_widget_input_window(widget, &priv->event_window);
    g_clear_object(&priv->context);
    g_clear_object(&priv->layout);
    GTK_WIDGET_CLASS(parent_class)->unrealize(widget);
}

static void
map(GtkWidget *widget)
{
    GwyRuler *ruler = GWY_RULER(widget);
    GwyRulerPrivate *priv = ruler->priv;

    parent_class->map(widget);

    if (priv->event_window)
        gdk_window_show(priv->event_window);
}

static void
unmap(GtkWidget *widget)
{
    GwyRuler *ruler = GWY_RULER(widget);
    GwyRulerPrivate *priv = ruler->priv;

    if (priv->event_window) {
        /* FIXME: Do we need to update the state in some other way? */
        gdk_window_hide(priv->event_window);
    }

    parent_class->unmap(widget);
}

static void
screen_changed(GtkWidget *widget, G_GNUC_UNUSED GdkScreen *previous_screen)
{
    update_pango(GWY_RULER(widget));
}

static void
size_allocate(GtkWidget *widget, GdkRectangle *allocation)
{
    GwyRuler *ruler = GWY_RULER(widget);
    GwyRulerPrivate *priv = ruler->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);
        update_value_format(ruler);
        priv->drawn_position_set = FALSE;
    }
}

static gboolean
draw(GtkWidget *widget, cairo_t *cr)
{
    GwyRuler *ruler = GWY_RULER(widget);
    GtkStyleContext *context = gwy_setup_widget_foreground_drawing(widget, cr);

    GdkRectangle allocation;
    gtk_widget_get_allocation(widget, &allocation);
    gtk_render_background(context, cr, 0, 0, allocation.width, allocation.height);

    draw_ticks(ruler, cr);

    GwyRulerPrivate *priv = ruler->priv;
    gdouble h = 0.5*priv->height;
    gdouble x = gwy_ruler_coord_real_to_widget(ruler, priv->position);
    if (priv->orientation == GTK_ORIENTATION_HORIZONTAL)
        gwy_cairo_triangle_down(cr, x, priv->height - 0.5*h, 0.5*h);
    else
        gwy_cairo_triangle_right(cr, priv->height - 0.5*h, x, 0.5*h);
    cairo_fill(cr);

    priv->drawn_position = priv->position;
    priv->drawn_position_set = TRUE;

    return FALSE;
}

/**
 * gwy_ruler_get_unit:
 * @ruler: A #GwyRuler.
 *
 * Returns the base units a ruler uses.
 *
 * Modify the returned unit object to change the ruler units.
 *
 * Returns: (transfer none):
 *          The units the rules uses.
 **/
GwyUnit*
gwy_ruler_get_unit(GwyRuler *ruler)
{
    g_return_val_if_fail(GWY_IS_RULER(ruler), NULL);
    return ruler->priv->unit;
}

/**
 * gwy_ruler_move_marker:
 * @ruler: A #GwyRuler.
 * @value: Value where to draw marker.
 *
 * Changes the position of a ruler marker.
 *
 * The position is also automatically updated when pointer is moved over the marker. This function is meant for
 * updating the ruler when pointer moves over other widgets.
 **/
void
gwy_ruler_move_marker(GwyRuler *ruler,
                      gdouble value)
{
    g_return_if_fail(GWY_IS_RULER(ruler));
    GwyRulerPrivate *priv = ruler->priv;
    if (value == priv->position)
        return;

    gboolean must_redraw = (CLAMP(value, priv->lower, priv->upper)
                            != CLAMP(priv->position, priv->lower, priv->upper));
    priv->position = value;
    if (must_redraw && gtk_widget_get_realized(GTK_WIDGET(ruler))) {
        queue_marker_redraw(ruler, value);
        if (priv->drawn_position_set)
            queue_marker_redraw(ruler, priv->drawn_position);
    }
    g_object_notify_by_pspec(G_OBJECT(ruler), properties[PROP_POSITION]);
}

/**
 * gwy_ruler_coord_real_to_widget:
 * @ruler: A #GwyRuler.
 * @value: Real value on the ruler.
 *
 * Transforms real coordinate to position on the ruler widget.
 *
 * Returns: Position on widget corresponding to the value.
 **/
gdouble
gwy_ruler_coord_real_to_widget(GwyRuler *ruler,
                               gdouble value)
{
    g_return_val_if_fail(GWY_IS_RULER(ruler), 0.0);
    GwyRulerPrivate *priv = ruler->priv;
    gdouble d = (priv->upper - priv->lower)/priv->pixelsize;
    gdouble x = (value - priv->lower)/d;
    return CLAMP(x, 0.0, priv->pixelsize);
}

/**
 * gwy_ruler_coord_widget_to_real:
 * @ruler: A #GwyRuler.
 * @pos: Pixel position on the ruler, in widget coordinates.
 *
 * Transforms pixel position a ruler to real coordinate value.
 *
 * Returns: Ruler value corresponding to widget position.
 **/
gdouble
gwy_ruler_coord_widget_to_real(GwyRuler *ruler,
                               gdouble pos)
{
    g_return_val_if_fail(GWY_IS_RULER(ruler), 0.0);
    GwyRulerPrivate *priv = ruler->priv;
    gdouble d = (priv->upper - priv->lower)/priv->pixelsize;
    return priv->lower + pos*d;
}

static void
set_min_sizes(GwyRuler *ruler)
{
    GwyRulerPrivate *priv = ruler->priv;
    PangoRectangle rect;

    PangoLayout *layout = gtk_widget_create_pango_layout(GTK_WIDGET(ruler), NULL);
    pango_layout_set_text(layout, "(|)<sup>(0)</sup><sub>(0)</sub>", -1);
    pango_layout_get_pixel_extents(layout, NULL, &rect);
    g_object_unref(layout);

    gint size = 2*LINE_WIDTH + 3*(PANGO_ASCENT(rect) + PANGO_DESCENT(rect))/2;
    if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) {
        priv->min_width = 2*LINE_WIDTH + 1;
        priv->min_height = size;
    }
    else {
        priv->min_width = size;
        priv->min_height = 2*LINE_WIDTH + 1;
    }
}

static void
update_pango(GwyRuler *ruler)
{
    GtkWidget *widget = GTK_WIDGET(ruler);
    if (!gtk_widget_get_realized(widget))
        return;

    GwyRulerPrivate *priv = ruler->priv;
    g_clear_object(&priv->layout);
    g_clear_object(&priv->context);
    priv->context = gtk_widget_create_pango_context(widget);
    if (priv->orientation == GTK_ORIENTATION_VERTICAL) {
        PangoMatrix matrix = PANGO_MATRIX_INIT;
        pango_matrix_rotate(&matrix, -90.0);
        pango_context_set_matrix(priv->context, &matrix);
    }
    priv->layout = pango_layout_new(priv->context);
}

static void
queue_marker_redraw(GwyRuler *ruler, gdouble value)
{
    GwyRulerPrivate *priv = ruler->priv;
    GdkRectangle allocation;
    gtk_widget_get_allocation(GTK_WIDGET(ruler), &allocation);

    gint h = (gint)ceil(0.5*priv->height) + 1;
    gint x = GWY_ROUND(gwy_ruler_coord_real_to_widget(ruler, value));
    gint x0, x1, y0, y1;
    if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) {
        x0 = CLAMP(x - h/2 - 1, 0, priv->pixelsize);
        x1 = CLAMP(x + h/2 + 1, 0, priv->pixelsize);
        y0 = MAX(priv->height - h, 0);
        y1 = priv->height;
    }
    else {
        x0 = MAX(priv->height - h, 0);
        x1 = priv->height;
        y0 = CLAMP(x - h/2 - 1, 0, priv->pixelsize);
        y1 = CLAMP(x + h/2 + 1, 0, priv->pixelsize);
    }
    gtk_widget_queue_draw_area(GTK_WIDGET(ruler), allocation.x + x0, allocation.y + y0, x1 - x0, y1 - y0);
}

static void
unit_changed(GwyRuler *ruler)
{
    update_value_format(ruler);
    if (gtk_widget_is_drawable(GTK_WIDGET(ruler)))
        gtk_widget_queue_draw(GTK_WIDGET(ruler));
}

static void
update_value_format(GwyRuler *ruler)
{
    GwyRulerPrivate *priv = ruler->priv;
    gdouble max;

    max = priv->max_size;
    if (!max)
        max = MAX(fabs(priv->lower), fabs(priv->upper));
    if (!max)
        max = 1.2;

    priv->vformat = gwy_unit_get_format_with_resolution(priv->unit, GWY_UNIT_FORMAT_VFMARKUP, max, max/21,
                                                        priv->vformat);
}

static void
draw_ticks(GwyRuler *ruler, cairo_t *cr)
{
    enum {
        FINALLY_OK,
        FIRST_TRY,
        ADD_DIGITS,
        LESS_TICKS
    };

    struct {
        ScaleBase scale;
        gdouble base;
    } tick_info[4];

    gint labels = 0, scale_depth;
    gdouble step, first, bstep;
    GwyRulerPrivate *priv = ruler->priv;
    gboolean unit_at_zero = (priv->units_placement == GWY_UNITS_PLACEMENT_AT_ZERO);
    PangoRectangle rect;
    GdkRectangle allocation;

    gtk_widget_get_allocation(GTK_WIDGET(ruler), &allocation);
    if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) {
        priv->hthickness = LINE_WIDTH;
        priv->vthickness = LINE_WIDTH;
        priv->height = allocation.height;
        priv->pixelsize = allocation.width;
    }
    else {
        priv->hthickness = LINE_WIDTH;
        priv->vthickness = LINE_WIDTH;
        priv->height = allocation.width;
        priv->pixelsize = allocation.height;
    }

    gint min_tick_spacing = MINIMUM_INCR;
    gint min_label_spacing = priv->hthickness + MINIMUM_INCR;

    GwyValueFormat *vf = priv->vformat;
    PangoLayout *layout = priv->layout;
    gdouble upper = priv->upper;
    gdouble lower = priv->lower;
    if (upper <= lower || priv->pixelsize < 2 || priv->pixelsize > 10000)
        return;
    gdouble max = priv->max_size;
    if (max == 0)
        max = MAX(fabs(lower), fabs(upper));

    gdouble range = upper - lower;
    gdouble measure = range/priv->pixelsize;

    GString *unit_str = g_string_new(NULL);
    ScaleBase scale = SCALE_1;
    gint state = FIRST_TRY;
    gint ascent = 0;
    gdouble base; // = sizeof("Die, die GCC warning!");
    do {
        if (state != LESS_TICKS) {
            if (unit_at_zero && *vf->units) {
                if (lower > 0)
                    g_string_printf(unit_str, "%.*f %s", vf->precision, lower/vf->magnitude, vf->units);
                else
                    g_string_printf(unit_str, "0 %s", vf->units);
            }
            else
                g_string_printf(unit_str, "%.*f", vf->precision, max/vf->magnitude);

            pango_layout_set_markup(layout, unit_str->str, unit_str->len);
            pango_layout_get_pixel_extents(layout, NULL, &rect);
            ascent = PANGO_ASCENT(rect);
            gint text_size = rect.width + 1;

            if (lower < 0) {
                g_string_printf(unit_str, "%.*f", vf->precision, lower/vf->magnitude);
                if (unit_str->str[0] == '-') {
                    g_string_erase(unit_str, 0, 1);
                    g_string_insert(unit_str, 0, "−");
                }
                pango_layout_set_markup(layout, unit_str->str, unit_str->len);
                pango_layout_get_pixel_extents(layout, NULL, &rect);
                if (text_size < rect.width + 1)
                    text_size = rect.width + 1;
            }

            /* fit as many labels as you can */
            labels = floor(priv->pixelsize/(text_size + priv->hthickness + min_label_spacing));
            labels = MAX(labels, 1);
            if (labels > 8)
                labels = 8 + (labels - 7)/2;

            step = range / labels;
            base = gwy_exp10(floor(log10(step) + 1e-12));
            step /= base;
            while (step <= 0.5) {
                base /= 10.0;
                step *= 10.0;
            }
            while (step > 5.0) {
                base *= 10.0;
                step /= 10.0;
            }
            if (step <= 1.0)
                scale = SCALE_1;
            else if (step <= 2.0)
                scale = SCALE_2;
            else if (step <= 2.5)
                scale = SCALE_2_5;
            else
                scale = SCALE_5;
        }

        step = scale_steps[scale];
        bstep = base*step;
        first = floor(lower / (bstep))*bstep;
        gwy_debug("%d first: %g, base: %g, step: %g, prec: %d, labels: %d",
                  state, first, base, step, vf->precision, labels);

        if (is_precision_ok(vf, base, scale)) {
            if (state == FIRST_TRY && vf->precision > 0)
                vf->precision--;
            else
                state = FINALLY_OK;
        }
        else {
            if (state == FIRST_TRY) {
                state = ADD_DIGITS;
                vf->precision++;
            }
            else {
                state = LESS_TICKS;
                base *= 10;
                scale = SCALE_1;
            }
        }
    } while (state != FINALLY_OK);

    /* draw labels */
    gboolean units_drawn = FALSE;
    for (gint i = 0; ; i++) {
        gint pos;
        gdouble val;

        val = i*bstep + first;
        pos = floor((val - lower)/measure);
        if (pos >= priv->pixelsize)
            break;
        if (pos < 0)
            continue;
        if (!units_drawn && (upper < 0 || val >= 0) && unit_at_zero && priv->unit) {
            if (val == 0)
                g_string_printf(unit_str, "0 %s", vf->units);
            else
                g_string_printf(unit_str, "%.*f %s", vf->precision, val/vf->magnitude, vf->units);
            units_drawn = TRUE;
        }
        else
            g_string_printf(unit_str, "%.*f", vf->precision, val/vf->magnitude);

        if (unit_str->str[0] == '-') {
            g_string_erase(unit_str, 0, 1);
            g_string_insert(unit_str, 0, "−");
        }

        pango_layout_set_markup(priv->layout, unit_str->str, unit_str->len);
        /* XXX: This is the best approximation of same positioning I'm able to do, but it's still wrong. */
        pango_layout_get_pixel_extents(priv->layout, NULL, &rect);

        GtkWidget *widget = GTK_WIDGET(ruler);
        GtkStyleContext *context = gtk_widget_get_style_context(widget);
        if (priv->orientation == GTK_ORIENTATION_HORIZONTAL) {
            gint vpos = priv->vthickness + ascent - PANGO_ASCENT(rect);
            gtk_render_layout(context, cr, pos + 3, vpos, priv->layout);
        }
        else {
            /* The (h|v)positions are swapped here becuase we had some wonky coordinate convention. */
            gint vpos = priv->vthickness + ascent + PANGO_DESCENT(rect);
            gtk_render_layout(context, cr, vpos, pos + 3, priv->layout);
        }
    }

    g_string_free(unit_str, TRUE);

    /* draw tick marks, from smallest to largest */
    scale_depth = 0;
    while (scale && scale_depth < (gint)G_N_ELEMENTS(tick_info)) {
        tick_info[scale_depth].scale = scale;
        tick_info[scale_depth].base = base;
        scale = next_scale(scale, &base, measure, min_tick_spacing);
        scale_depth++;
    }
    scale_depth--;

    cairo_set_line_width(cr, 1.0);
    cairo_set_line_cap(cr, CAIRO_LINE_CAP_BUTT);
    while (scale_depth > -1) {
        gint tick_length = priv->height/(scale_depth + 1) - 3;
        scale = tick_info[scale_depth].scale;
        base = tick_info[scale_depth].base;
        bstep = base*scale_steps[scale];
        first = floor(lower/bstep)*bstep;
        for (gint i = 0; ; i++) {
            gint pos;
            gdouble val;

            val = (i + 0.000001)*bstep + first;
            pos = floor((val - lower)/measure);
            if (pos >= priv->pixelsize)
                break;
            if (pos < 0)
                continue;

            if (priv->orientation == GTK_ORIENTATION_HORIZONTAL)
                gwy_cairo_line(cr, pos + 0.5, priv->height, pos + 0.5, priv->height - tick_length);
            else
                gwy_cairo_line(cr, priv->height, pos + 0.5, priv->height - tick_length, pos + 0.5);
        }
        scale_depth--;
    }
    cairo_stroke(cr);
}

static gboolean
is_precision_ok(const GwyValueFormat *format,
                gdouble base,
                ScaleBase scale)
{
    gint m = GWY_ROUND(log10(base/format->magnitude));

    return format->precision + m >= (scale == SCALE_2_5);
}

static ScaleBase
next_scale(ScaleBase scale,
           gdouble *base,
           gdouble measure,
           gint min_incr)
{
    ScaleBase new_scale = SCALE_0;

    switch (scale) {
        case SCALE_1:
        *base /= 10.0;
        if ((gint)floor(*base*2.0/measure) > min_incr)
            new_scale = SCALE_5;
        else if ((gint)floor(*base*2.5/measure) > min_incr)
            new_scale = SCALE_2_5;
        else if ((gint)floor(*base*5.0/measure) > min_incr)
            new_scale = SCALE_5;
        break;

        case SCALE_2:
        if ((gint)floor(*base/measure) > min_incr)
            new_scale = SCALE_1;
        break;

        case SCALE_2_5:
        *base /= 10.0;
        if ((gint)floor(*base*5.0/measure) > min_incr)
            new_scale = SCALE_5;
        break;

        case SCALE_5:
        if ((gint)floor(*base/measure) > min_incr)
            new_scale = SCALE_1;
        else if ((gint)floor(*base*2.5/measure) > min_incr)
            new_scale = SCALE_2_5;
        break;

        default:
        g_assert_not_reached();
        break;
    }

    return new_scale;
}

/**
 * SECTION: ruler
 * @title: GwyRuler
 * @short_description: Ruler with units
 *
 * #GwyRuler is a ruler similar to #GtkRuler, but it is more suited for a scientific application.  It is
 * scale-independent and thus has no arbitrary limits on the ranges or interpretation of displayed values.  It can
 * display units on the ruler (this can be controlled with gwy_ruler_set_units_placement()) and cooperates with
 * #GwyUnit (see gwy_ruler_get_unit()).
 **/

/**
 * GwyUnitsPlacement:
 * @GWY_UNITS_PLACEMENT_NONE: Units are omitted.
 * @GWY_UNITS_PLACEMENT_AT_ZERO: Units are placed to major tick at zero, or to the leftmost position of zero is not
 *                               present.
 *
 * Units placement on a #GwyRuler.
 **/

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