6 n__gui__colorwheel_8c
gullradriel edited this page 2026-10-09 00:55:18 +02:00

src/n_gui_colorwheel.c

A colour wheel for N_GUI: one point in a disc, dragged. More...

Structures

Name
struct _N_GUI_COLORWHEEL_MOUSE
what a wheel's mouse callback needs: the context and the state
struct _N_GUI_COLORWHEEL_BLOCK
a wheel's state and its mouse callback's context in one allocation, because a custom widget owns one pointer
struct _N_GUI_COLORWHEEL
a wheel's state, owned by its custom widget

Functions

Name
_N_GUI_COLORWHEEL * _wheel_of(N_GUI_CTX * ctx, int widget_id)
helper: find the wheel behind a widget id, or NULL
int _wheel_mouse(int widget_id, const N_GUI_MOUSE_EVENT * ev, void * user_data)
custom mouse callback
void _wheel_geometry(float x, float y, float w, float h, float * cx, float * cy, float * radius)
helper: the disc's centre and radius in a widget of w by h at x, y
void _wheel_draw(N_GUI_WIDGET * widget, float x, float y, ALLEGRO_FONT * font, void * user_data)
custom draw callback
void _wheel_clamp(float * u, float * v)
helper: the point kept inside the unit circle
void _wheel_block_destroy(void * user_data)
destroy callback for the block

Defines

Name
N_GUI_COLORWHEEL_SECTORS
rings and sectors the disc is drawn with: enough to look smooth at the sizes a panel gives it
N_GUI_COLORWHEEL_RINGS
rings and sectors the disc is drawn with: enough to look smooth at the sizes a panel gives it
N_GUI_COLORWHEEL_PI
pi, not taken from math.h's M_PI, which strict C does not have

Detailed Description

A colour wheel for N_GUI: one point in a disc, dragged.

Author: Castagnier Mickael

Version: 1.0

Date: 28/09/2026

Functions Documentation

function _wheel_of

static _N_GUI_COLORWHEEL * _wheel_of(
    N_GUI_CTX * ctx,
    int widget_id
)

helper: find the wheel behind a widget id, or NULL

function _wheel_mouse

static int _wheel_mouse(
    int widget_id,
    const N_GUI_MOUSE_EVENT * ev,
    void * user_data
)

custom mouse callback

function _wheel_geometry

static void _wheel_geometry(
    float x,
    float y,
    float w,
    float h,
    float * cx,
    float * cy,
    float * radius
)

helper: the disc's centre and radius in a widget of w by h at x, y

function _wheel_draw

static void _wheel_draw(
    N_GUI_WIDGET * widget,
    float x,
    float y,
    ALLEGRO_FONT * font,
    void * user_data
)

custom draw callback

function _wheel_clamp

static void _wheel_clamp(
    float * u,
    float * v
)

helper: the point kept inside the unit circle

function _wheel_block_destroy

static void _wheel_block_destroy(
    void * user_data
)

destroy callback for the block

Macros Documentation

define N_GUI_COLORWHEEL_SECTORS

#define N_GUI_COLORWHEEL_SECTORS 48

rings and sectors the disc is drawn with: enough to look smooth at the sizes a panel gives it

define N_GUI_COLORWHEEL_RINGS

#define N_GUI_COLORWHEEL_RINGS 8

rings and sectors the disc is drawn with: enough to look smooth at the sizes a panel gives it

define N_GUI_COLORWHEEL_PI

#define N_GUI_COLORWHEEL_PI 3.14159265358979f

pi, not taken from math.h's M_PI, which strict C does not have

Source code

/*
 * Nilorea Library
 * Copyright (C) 2005-2026 Castagnier Mickael
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 * implied. See the License for the specific language governing
 * permissions and limitations under the License.
 *
 * SPDX-License-Identifier: Apache-2.0
 */


#ifdef HAVE_ALLEGRO

#include <math.h>
#include <stdlib.h>
#include <string.h>

#include <allegro5/allegro_color.h>
#include <allegro5/allegro_primitives.h>

#include "nilorea/n_common.h"
#include "nilorea/n_log.h"
#include "nilorea/n_gui_colorwheel.h"

#define N_GUI_COLORWHEEL_PI 3.14159265358979f

#define N_GUI_COLORWHEEL_RINGS 8
#define N_GUI_COLORWHEEL_SECTORS 48

typedef struct {
    float u;
    float v;
    int dragging;
    N_GUI_COLORWHEEL_CHANGE on_change;
    void* user_data;
    N_GUI_COLORWHEEL_COLOUR colour;
    void* colour_user_data;
    double press_time;
    float press_x;
    float press_y;
} _N_GUI_COLORWHEEL;

ALLEGRO_COLOR n_gui_colorwheel_hue(float u, float v, void* user_data) {
    float s = sqrtf(u * u + v * v);
    float h = atan2f(v, u) * 180.0f / N_GUI_COLORWHEEL_PI;
    float r = 0.0f, g = 0.0f, b = 0.0f;

    (void)user_data;
    if (h < 0.0f) h += 360.0f;
    if (s > 1.0f) s = 1.0f;
    al_color_hsv_to_rgb(h, s, 1.0f, &r, &g, &b);
    return al_map_rgb_f(r, g, b);
}

static void _wheel_clamp(float* u, float* v) {
    float d = sqrtf(*u * *u + *v * *v);

    if (d > 1.0f) {
        *u /= d;
        *v /= d;
    }
}

static void _wheel_geometry(float x, float y, float w, float h, float* cx, float* cy, float* radius) {
    *radius = (w < h ? w : h) * 0.5f - 2.0f;
    if (*radius < 1.0f) *radius = 1.0f;
    *cx = x + w * 0.5f;
    *cy = y + h * 0.5f;
}

// cppcheck-suppress constParameterCallback ; callback signature must match N_GUI_CUSTOM_DRAW
// cppcheck-suppress constParameter ; the same, under the id cppcheck 2.10 uses
static void _wheel_draw(N_GUI_WIDGET* widget, float x, float y, ALLEGRO_FONT* font, void* user_data) {
    const _N_GUI_COLORWHEEL* cw = (const _N_GUI_COLORWHEEL*)user_data;
    float cx = 0.0f, cy = 0.0f, radius = 0.0f;
    int ring = 0, sector;

    (void)font;
    if (!widget || !cw) return;
    _wheel_geometry(x, y, widget->w, widget->h, &cx, &cy, &radius);
    /* The disc as quads, each coloured at its corners with the caller's
     * colour there, so the shading follows whatever the point means. */
    for (ring = 0; ring < N_GUI_COLORWHEEL_RINGS; ring++) {
        float r0 = (float)ring / N_GUI_COLORWHEEL_RINGS, r1 = (float)(ring + 1) / N_GUI_COLORWHEEL_RINGS;

        for (sector = 0; sector < N_GUI_COLORWHEEL_SECTORS; sector++) {
            float a0 = 2.0f * N_GUI_COLORWHEEL_PI * (float)sector / N_GUI_COLORWHEEL_SECTORS;
            float a1 = 2.0f * N_GUI_COLORWHEEL_PI * (float)(sector + 1) / N_GUI_COLORWHEEL_SECTORS;
            float us[4] = {r0 * cosf(a0), r1 * cosf(a0), r1 * cosf(a1), r0 * cosf(a1)};
            float vs[4] = {r0 * sinf(a0), r1 * sinf(a0), r1 * sinf(a1), r0 * sinf(a1)};
            ALLEGRO_VERTEX vtx[6];
            static const int order[6] = {0, 1, 2, 0, 2, 3};
            int k = 0;

            for (k = 0; k < 6; k++) {
                int c = order[k];

                memset(&vtx[k], 0, sizeof(vtx[k]));
                vtx[k].x = cx + us[c] * radius;
                vtx[k].y = cy - vs[c] * radius;
                vtx[k].color = cw->colour(us[c], vs[c], cw->colour_user_data);
            }
            al_draw_prim(vtx, NULL, NULL, 0, 6, ALLEGRO_PRIM_TRIANGLE_LIST);
        }
    }
    al_draw_circle(cx, cy, radius, widget->theme.border_normal, 1.0f);
    al_draw_line(cx - radius, cy, cx + radius, cy, widget->theme.border_normal, 1.0f);
    al_draw_line(cx, cy - radius, cx, cy + radius, widget->theme.border_normal, 1.0f);
    {
        float px = cx + cw->u * radius, py = cy - cw->v * radius;
        float r = radius * 0.07f < 3.0f ? 3.0f : radius * 0.07f;

        al_draw_filled_circle(px, py, r, widget->theme.text_normal);
        al_draw_circle(px, py, r, widget->theme.bg_normal, 1.5f);
    }
}

static _N_GUI_COLORWHEEL* _wheel_of(N_GUI_CTX* ctx, int widget_id) {
    const N_GUI_WIDGET* wgt = ctx ? n_gui_get_widget(ctx, widget_id) : NULL;
    const N_GUI_CUSTOM_DATA* cd = NULL;

    if (!wgt || wgt->type != N_GUI_TYPE_CUSTOM || !wgt->data) return NULL;
    cd = (const N_GUI_CUSTOM_DATA*)wgt->data;
    /* the paint callback says what the widget is */
    if (cd->draw != _wheel_draw) return NULL;
    return (_N_GUI_COLORWHEEL*)cd->user_data;
}

typedef struct {
    N_GUI_CTX* ctx;
    _N_GUI_COLORWHEEL* cw;
} _N_GUI_COLORWHEEL_MOUSE;

// cppcheck-suppress constParameterCallback ; callback signature must match N_GUI_CUSTOM_MOUSE
// cppcheck-suppress constParameter ; the same, under the id cppcheck 2.10 uses
static int _wheel_mouse(int widget_id, const N_GUI_MOUSE_EVENT* ev, void* user_data) {
    const _N_GUI_COLORWHEEL_MOUSE* cm = (const _N_GUI_COLORWHEEL_MOUSE*)user_data;
    _N_GUI_COLORWHEEL* cw = cm ? cm->cw : NULL;
    const N_GUI_WIDGET* wgt = cm ? n_gui_get_widget(cm->ctx, widget_id) : NULL;
    float cx = 0.0f, cy = 0.0f, radius = 0.0f;

    if (!cw || !wgt || wgt->w <= 0.0f || wgt->h <= 0.0f) return 0;
    _wheel_geometry(0.0f, 0.0f, wgt->w, wgt->h, &cx, &cy, &radius);
    if (ev->type == N_GUI_MOUSE_DOWN && ev->button == 2) {
        cw->u = 0.0f;
        cw->v = 0.0f;
        if (cw->on_change) cw->on_change(widget_id, 0, cw->user_data);
        n_gui_mark_dirty(cm->ctx);
        return 1;
    }
    if (ev->type == N_GUI_MOUSE_DOWN && ev->button == 1) {
        /* A second press soon after and near the first is a double click:
         * the point goes back to the centre, as a slider with a default goes
         * back to it, and no drag starts, so the release does not move it
         * away again. The context's own timing, the one n_gui_was_double_
         * click() reports by. */
        double now = al_get_time();
        float dx = ev->x - cw->press_x, dy = ev->y - cw->press_y;
        float near_px = (float)cm->ctx->double_click_radius;

        if (cw->press_time > 0.0 && now - cw->press_time <= cm->ctx->double_click_time &&
            dx * dx + dy * dy <= near_px * near_px) {
            cw->press_time = 0.0;
            cw->dragging = 0;
            cw->u = 0.0f;
            cw->v = 0.0f;
            if (cw->on_change) cw->on_change(widget_id, 0, cw->user_data);
            n_gui_mark_dirty(cm->ctx);
            return 1;
        }
        cw->press_time = now;
        cw->press_x = ev->x;
        cw->press_y = ev->y;
    }
    if ((ev->type == N_GUI_MOUSE_DOWN && ev->button == 1) || (ev->type == N_GUI_MOUSE_DRAG && cw->dragging)) {
        cw->u = (ev->x - cx) / radius;
        cw->v = (cy - ev->y) / radius;
        _wheel_clamp(&cw->u, &cw->v);
        cw->dragging = 1;
        if (cw->on_change) cw->on_change(widget_id, 1, cw->user_data);
        n_gui_mark_dirty(cm->ctx);
        return 1;
    }
    if (ev->type == N_GUI_MOUSE_UP && cw->dragging) {
        cw->dragging = 0;
        if (cw->on_change) cw->on_change(widget_id, 0, cw->user_data);
        return 1;
    }
    return 0;
}

typedef struct {
    _N_GUI_COLORWHEEL cw;
    _N_GUI_COLORWHEEL_MOUSE mouse;
} _N_GUI_COLORWHEEL_BLOCK;

static void _wheel_block_destroy(void* user_data) {
    free(user_data);
}

int n_gui_colorwheel_new(N_GUI_CTX* ctx, int window_id, float x, float y, float w, float h, N_GUI_COLORWHEEL_CHANGE on_change, void* user_data) {
    _N_GUI_COLORWHEEL_BLOCK* block = NULL;
    int id = -1;

    __n_assert(ctx, return -1);
    block = (_N_GUI_COLORWHEEL_BLOCK*)calloc(1, sizeof(*block));
    if (!block) return -1;
    block->cw.on_change = on_change;
    block->cw.user_data = user_data;
    block->cw.colour = n_gui_colorwheel_hue;
    block->mouse.ctx = ctx;
    block->mouse.cw = &block->cw;
    /* the block's first member is the state, so the draw callback's pointer
     * and the block's are the same pointer */
    id = n_gui_add_custom(ctx, window_id, x, y, w, h, _wheel_draw, &block->cw);
    if (id < 0) {
        free(block);
        return -1;
    }
    n_gui_custom_set_destroy(ctx, id, _wheel_block_destroy);
    n_gui_custom_set_mouse(ctx, id, _wheel_mouse, &block->mouse);
    return id;
}

int n_gui_colorwheel_set(N_GUI_CTX* ctx, int widget_id, float u, float v) {
    _N_GUI_COLORWHEEL* cw = _wheel_of(ctx, widget_id);

    if (!cw || cw->dragging) return FALSE;
    _wheel_clamp(&u, &v);
    cw->u = u;
    cw->v = v;
    n_gui_mark_dirty(ctx);
    return TRUE;
}

int n_gui_colorwheel_get(N_GUI_CTX* ctx, int widget_id, float* u, float* v) {
    const _N_GUI_COLORWHEEL* cw = _wheel_of(ctx, widget_id);

    if (!cw) return FALSE;
    if (u) *u = cw->u;
    if (v) *v = cw->v;
    return TRUE;
}

void n_gui_colorwheel_set_colour(N_GUI_CTX* ctx, int widget_id, N_GUI_COLORWHEEL_COLOUR colour, void* user_data) {
    _N_GUI_COLORWHEEL* cw = _wheel_of(ctx, widget_id);

    if (!cw) return;
    cw->colour = colour ? colour : n_gui_colorwheel_hue;
    cw->colour_user_data = colour ? user_data : NULL;
    n_gui_mark_dirty(ctx);
}

#endif /* HAVE_ALLEGRO */