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

src/n_gui_color.c

A colour picker dialog for N_GUI, and a swatch to show a colour. More...

Structures

Name
struct N_GUI_COLOR_PICKER
a colour picker: the ids of its widgets and the colour being edited

Functions

Name
void _n_gui_color_set_label(N_GUI_COLOR_PICKER * picker, int channel)
helper: write one channel's label, its name and its value out of 255
void _n_gui_color_on_slider(int widget_id, double value, void * user_data)
slider callback: one channel moved
void _n_gui_color_on_close_window(int window_id, void * user_data)
title bar close: the same as the Close button, so the callback is forgotten either way
void _n_gui_color_on_close_button(int widget_id, void * user_data)
button callback: Close
void _n_gui_color_draw_swatch(N_GUI_WIDGET * widget, float x, float y, ALLEGRO_FONT * font, void * user_data)
custom draw callback of the large swatch
float _n_gui_color_clamp(float v)
helper: clamp a channel to 0..1

Attributes

Name
const char *const[4] _n_gui_color_channels
the four channels, in the order of the sliders

Defines

Name
N_GUI_COLOR_W
window width at scale 1: wide enough that a slider moved by one step in 255 moves by at least a pixel
N_GUI_COLOR_SWATCH
height of the large swatch at scale 1
N_GUI_COLOR_ROW
height of a slider or button row at scale 1
N_GUI_COLOR_PAD
padding at scale 1
N_GUI_COLOR_LABEL
height of a label at scale 1
N_GUI_COLOR_CHECK
side of one checkerboard square behind a swatch, in pixels.
N_GUI_COLOR_BTN_W
width of the Close button at scale 1

Detailed Description

A colour picker dialog for N_GUI, and a swatch to show a colour.

Author: Castagnier Mickael

Version: 1.0

Date: 24/09/2026

Functions Documentation

function _n_gui_color_set_label

static void _n_gui_color_set_label(
    N_GUI_COLOR_PICKER * picker,
    int channel
)

helper: write one channel's label, its name and its value out of 255

function _n_gui_color_on_slider

static void _n_gui_color_on_slider(
    int widget_id,
    double value,
    void * user_data
)

slider callback: one channel moved

function _n_gui_color_on_close_window

static void _n_gui_color_on_close_window(
    int window_id,
    void * user_data
)

title bar close: the same as the Close button, so the callback is forgotten either way

function _n_gui_color_on_close_button

static void _n_gui_color_on_close_button(
    int widget_id,
    void * user_data
)

button callback: Close

function _n_gui_color_draw_swatch

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

custom draw callback of the large swatch

function _n_gui_color_clamp

static float _n_gui_color_clamp(
    float v
)

helper: clamp a channel to 0..1

Attributes Documentation

variable _n_gui_color_channels

static const char *const[4] _n_gui_color_channels = {"Red", "Green", "Blue", "Alpha"};

the four channels, in the order of the sliders

Macros Documentation

define N_GUI_COLOR_W

#define N_GUI_COLOR_W 300.0f

window width at scale 1: wide enough that a slider moved by one step in 255 moves by at least a pixel

define N_GUI_COLOR_SWATCH

#define N_GUI_COLOR_SWATCH 48.0f

height of the large swatch at scale 1

define N_GUI_COLOR_ROW

#define N_GUI_COLOR_ROW 24.0f

height of a slider or button row at scale 1

define N_GUI_COLOR_PAD

#define N_GUI_COLOR_PAD 10.0f

padding at scale 1

define N_GUI_COLOR_LABEL

#define N_GUI_COLOR_LABEL 18.0f

height of a label at scale 1

define N_GUI_COLOR_CHECK

#define N_GUI_COLOR_CHECK 6.0f

side of one checkerboard square behind a swatch, in pixels.

Not scaled: it only has to show that something is see-through.

define N_GUI_COLOR_BTN_W

#define N_GUI_COLOR_BTN_W 90.0f

width of the Close button at scale 1

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 <stdio.h>
#include <string.h>

#include <allegro5/allegro_primitives.h>

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

#define N_GUI_COLOR_W 300.0f
#define N_GUI_COLOR_ROW 24.0f
#define N_GUI_COLOR_LABEL 18.0f
#define N_GUI_COLOR_SWATCH 48.0f
#define N_GUI_COLOR_PAD 10.0f
#define N_GUI_COLOR_BTN_W 90.0f
#define N_GUI_COLOR_CHECK 6.0f

static const char* const _n_gui_color_channels[4] = {"Red", "Green", "Blue", "Alpha"};

struct N_GUI_COLOR_PICKER {
    N_GUI_CTX* ctx;
    int window;
    int lbl_what;
    int swatch;
    int lbl[4];
    int sl[4];
    int btn_close;
    float rgba[4];
    int syncing;
    N_GUI_COLOR_FN on_change;
    void* user_data;
};

static float _n_gui_color_clamp(float v) {
    return v < 0.0f ? 0.0f : (v > 1.0f ? 1.0f : v);
}

ALLEGRO_MOUSE_CURSOR* n_gui_eyedropper_cursor(float scale) {
    ALLEGRO_MOUSE_CURSOR* cursor = NULL;
    ALLEGRO_BITMAP* bitmap = NULL;
    ALLEGRO_BITMAP* previous = al_get_target_bitmap();
    ALLEGRO_STATE state;
    int size = (int)(32.0f * scale + 0.5f);
    float s = 0.0f, tip_x = 0.0f, tip_y = 0.0f, neck_x = 0.0f, neck_y = 0.0f, bulb_x = 0.0f, bulb_y = 0.0f;

    if (size < 16) size = 16;
    s = (float)size / 32.0f;
    al_store_state(&state, ALLEGRO_STATE_NEW_BITMAP_PARAMETERS | ALLEGRO_STATE_BLENDER);
    /* A memory bitmap: a cursor is copied out of it by the system, and a
       video one would tie this to whichever display is current. */
    al_set_new_bitmap_flags(ALLEGRO_MEMORY_BITMAP);
    bitmap = al_create_bitmap(size, size);
    if (!bitmap) {
        al_restore_state(&state);
        n_log(LOG_ERR, "eyedropper cursor: could not create a %dx%d bitmap", size, size);
        return NULL;
    }
    al_set_target_bitmap(bitmap);
    al_clear_to_color(al_map_rgba(0, 0, 0, 0));
    al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);

    /* Along the diagonal from the tip, bottom left, to the bulb, top right. */
    tip_x = 1.5f * s;
    tip_y = (float)size - 1.5f * s;
    neck_x = 21.0f * s;
    neck_y = 11.0f * s;
    bulb_x = 25.5f * s;
    bulb_y = 6.5f * s;
    /* The tube: a black outline under a pale fill, the fill narrower, so a
       black line shows around it on a light photograph and the pale one on
       a dark photograph. */
    al_draw_line(tip_x, tip_y, neck_x, neck_y, al_map_rgb(0, 0, 0), 6.0f * s);
    al_draw_line(tip_x + 1.0f * s, tip_y - 1.0f * s, neck_x, neck_y, al_map_rgb(235, 240, 245), 3.0f * s);
    /* The collar where the tube meets the bulb, across the diagonal. */
    al_draw_line(neck_x - 3.5f * s, neck_y - 3.5f * s, neck_x + 3.5f * s, neck_y + 3.5f * s, al_map_rgb(0, 0, 0), 4.0f * s);
    al_draw_line(neck_x - 2.5f * s, neck_y - 2.5f * s, neck_x + 2.5f * s, neck_y + 2.5f * s, al_map_rgb(235, 240, 245), 1.5f * s);
    /* The bulb. */
    al_draw_filled_circle(bulb_x, bulb_y, 6.0f * s, al_map_rgb(255, 255, 255));
    al_draw_filled_circle(bulb_x, bulb_y, 5.0f * s, al_map_rgb(0, 0, 0));
    al_draw_filled_circle(bulb_x, bulb_y, 3.5f * s, al_map_rgb(70, 70, 80));
    /* The tip, one dark pixel where the colour is read. */
    al_draw_filled_rectangle(0.0f, tip_y - 1.5f * s, 2.0f * s, (float)size, al_map_rgb(0, 0, 0));

    al_set_target_bitmap(previous);
    al_restore_state(&state);
    cursor = al_create_mouse_cursor(bitmap, 0, size - 1);
    al_destroy_bitmap(bitmap);
    if (!cursor) n_log(LOG_ERR, "eyedropper cursor: Allegro could not make a cursor");
    return cursor;
}

void n_gui_draw_color_swatch(float x, float y, float w, float h, const float rgba[4]) {
    int rows = 0, cols = 0, row = 0;

    if (!rgba || w <= 0.0f || h <= 0.0f) return;
    /* The checkerboard first, so an alpha below one shows as squares coming
     * through rather than as a darker colour. Whole squares, the last of each
     * row and column cut at the swatch's edge. */
    rows = (int)ceilf(h / N_GUI_COLOR_CHECK);
    cols = (int)ceilf(w / N_GUI_COLOR_CHECK);
    for (row = 0; row < rows; row++) {
        float cy = y + (float)row * N_GUI_COLOR_CHECK;
        float y2 = cy + N_GUI_COLOR_CHECK < y + h ? cy + N_GUI_COLOR_CHECK : y + h;

        for (int col = 0; col < cols; col++) {
            float cx = x + (float)col * N_GUI_COLOR_CHECK;
            float x2 = cx + N_GUI_COLOR_CHECK < x + w ? cx + N_GUI_COLOR_CHECK : x + w;
            float grey = ((row + col) & 1) ? 0.8f : 0.55f;

            al_draw_filled_rectangle(cx, cy, x2, y2, al_map_rgb_f(grey, grey, grey));
        }
    }
    /* Allegro's default blender takes premultiplied colour. */
    al_draw_filled_rectangle(x, y, x + w, y + h, al_premul_rgba_f(_n_gui_color_clamp(rgba[0]), _n_gui_color_clamp(rgba[1]), _n_gui_color_clamp(rgba[2]), _n_gui_color_clamp(rgba[3])));
    al_draw_rectangle(x + 0.5f, y + 0.5f, x + w - 0.5f, y + h - 0.5f, al_map_rgba(0, 0, 0, 160), 1.0f);
}

void n_gui_color_to_hex(const float rgba[4], char* out, size_t len) {
    unsigned byte[4] = {0, 0, 0, 0};
    int c = 0;

    if (!out || len == 0) return;
    if (!rgba) {
        out[0] = '\0';
        return;
    }
    for (c = 0; c < 4; c++) byte[c] = (unsigned)lroundf(_n_gui_color_clamp(rgba[c]) * 255.0f);
    snprintf(out, len, "#%02X%02X%02X%02X", byte[0], byte[1], byte[2], byte[3]);
}

// 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 _n_gui_color_draw_swatch(N_GUI_WIDGET* widget, float x, float y, ALLEGRO_FONT* font, void* user_data) {
    const N_GUI_COLOR_PICKER* picker = (const N_GUI_COLOR_PICKER*)user_data;

    (void)font;
    if (!widget || !picker) return;
    n_gui_draw_color_swatch(x, y, widget->w, widget->h, picker->rgba);
}

static void _n_gui_color_set_label(N_GUI_COLOR_PICKER* picker, int channel) {
    char text[32] = "";

    snprintf(text, sizeof(text), "%s %ld", _n_gui_color_channels[channel], lroundf(picker->rgba[channel] * 255.0f));
    n_gui_label_set_text(picker->ctx, picker->lbl[channel], text);
}

static void _n_gui_color_on_slider(int widget_id, double value, void* user_data) {
    N_GUI_COLOR_PICKER* picker = (N_GUI_COLOR_PICKER*)user_data;
    int c = 0;

    if (!picker || picker->syncing) return;
    for (c = 0; c < 4; c++) {
        if (picker->sl[c] != widget_id) continue;
        /* in steps of 1/255, so the number on the label is the one kept */
        picker->rgba[c] = (float)lround(value) / 255.0f;
        _n_gui_color_set_label(picker, c);
        if (picker->on_change) picker->on_change(picker->rgba, picker->user_data);
        n_gui_mark_dirty(picker->ctx);
        return;
    }
}

static void _n_gui_color_on_close_button(int widget_id, void* user_data) {
    (void)widget_id;
    n_gui_color_picker_close((N_GUI_COLOR_PICKER*)user_data);
}

static void _n_gui_color_on_close_window(int window_id, void* user_data) {
    (void)window_id;
    n_gui_color_picker_close((N_GUI_COLOR_PICKER*)user_data);
}

N_GUI_COLOR_PICKER* n_gui_color_picker_new(N_GUI_CTX* ctx, const char* title, float scale) {
    N_GUI_COLOR_PICKER* picker = NULL;
    const N_GUI_WINDOW* win = NULL;
    float pad = 0.0f, row = 0.0f, label = 0.0f, swatch_h = 0.0f, client_w = 0.0f, client_h = 0.0f, inner = 0.0f, y = 0.0f;
    int c = 0;

    __n_assert(ctx, return NULL);
    if (scale <= 0.0f) scale = 1.0f;
    Malloc(picker, N_GUI_COLOR_PICKER, 1);
    __n_assert(picker, return NULL);
    picker->ctx = ctx;

    pad = N_GUI_COLOR_PAD * scale;
    row = N_GUI_COLOR_ROW * scale;
    label = N_GUI_COLOR_LABEL * scale;
    swatch_h = N_GUI_COLOR_SWATCH * scale;
    client_w = N_GUI_COLOR_W * scale;
    inner = client_w - 2.0f * pad;
    /* what it is for, the swatch, four labelled sliders, Close */
    client_h = pad + label + swatch_h + pad + 4.0f * (label + row) + pad + row + pad;

    picker->window = n_gui_add_window(ctx, title ? title : "Colour", 0.0f, 0.0f, client_w, client_h);
    if (picker->window < 0) {
        Free(picker);
        return NULL;
    }
    n_gui_window_set_flags(ctx, picker->window, N_GUI_WIN_BTN_CLOSE);
    n_gui_window_set_zorder(ctx, picker->window, N_GUI_ZORDER_POPUP, 0);
    n_gui_window_set_close_callback(ctx, picker->window, _n_gui_color_on_close_window, picker);
    /* n_gui_add_window's height includes the title bar; make room for it */
    win = n_gui_get_window(ctx, picker->window);
    if (win) n_gui_window_set_rect(ctx, picker->window, 0.0f, 0.0f, client_w, client_h + win->titlebar_h);

    y = pad;
    picker->lbl_what = n_gui_add_label(ctx, picker->window, "", pad, y, inner, label, N_GUI_ALIGN_LEFT);
    y += label;
    picker->swatch = n_gui_add_custom(ctx, picker->window, pad, y, inner, swatch_h, _n_gui_color_draw_swatch, picker);
    y += swatch_h + pad;
    for (c = 0; c < 4; c++) {
        picker->lbl[c] = n_gui_add_label(ctx, picker->window, _n_gui_color_channels[c], pad, y, inner, label, N_GUI_ALIGN_LEFT);
        y += label;
        picker->sl[c] = n_gui_add_slider(ctx, picker->window, pad, y, inner, row, 0.0, 255.0, 255.0, N_GUI_SLIDER_VALUE, _n_gui_color_on_slider, picker);
        y += row;
        if (picker->lbl[c] < 0 || picker->sl[c] < 0) break;
        n_gui_slider_set_step(ctx, picker->sl[c], 1.0);
        n_gui_slider_set_value_visible(ctx, picker->sl[c], 0);
        n_gui_set_widget_tooltip(ctx, picker->sl[c], c < 3 ? "How much of this colour, 0 to 255" : "How solid, 0 see-through to 255 opaque");
    }
    y += pad;
    picker->btn_close = n_gui_add_button(ctx, picker->window, "Close", client_w - pad - N_GUI_COLOR_BTN_W * scale, y, N_GUI_COLOR_BTN_W * scale, row, N_GUI_SHAPE_ROUNDED, _n_gui_color_on_close_button, picker);
    n_gui_close_window(ctx, picker->window);
    if (c < 4 || picker->lbl_what < 0 || picker->swatch < 0 || picker->btn_close < 0) {
        n_log(LOG_ERR, "color picker: a widget could not be created");
        /* the window stays in the context, closed; nothing in it can reach
         * the freed handle while it is closed except its close callback */
        n_gui_window_set_close_callback(ctx, picker->window, NULL, NULL);
        Free(picker);
        return NULL;
    }
    n_gui_set_widget_tooltip(ctx, picker->btn_close, "Every change is already applied; this only closes the window");
    for (c = 0; c < 4; c++) picker->rgba[c] = 1.0f;
    return picker;
}

void n_gui_color_picker_open(N_GUI_COLOR_PICKER* picker, const char* what, const float rgba[4], N_GUI_COLOR_FN on_change, void* user_data) {
    const N_GUI_WINDOW* win = NULL;
    float dw = 0.0f, dh = 0.0f;
    int c = 0;

    __n_assert(picker, return);
    __n_assert(rgba, return);
    for (c = 0; c < 4; c++) picker->rgba[c] = _n_gui_color_clamp(rgba[c]);
    picker->on_change = on_change;
    picker->user_data = user_data;
    n_gui_label_set_text(picker->ctx, picker->lbl_what, what ? what : "");
    /* set from code: the callbacks the sliders fire are not the user's */
    picker->syncing = 1;
    for (c = 0; c < 4; c++) {
        n_gui_slider_set_value(picker->ctx, picker->sl[c], (double)lroundf(picker->rgba[c] * 255.0f));
        _n_gui_color_set_label(picker, c);
    }
    picker->syncing = 0;

    /* centred on the display: the context's size when it has one, else the
     * current display's, else left where it is */
    dw = picker->ctx->display_w;
    dh = picker->ctx->display_h;
    if ((dw <= 0.0f || dh <= 0.0f) && al_get_current_display()) {
        dw = (float)al_get_display_width(al_get_current_display());
        dh = (float)al_get_display_height(al_get_current_display());
    }
    win = n_gui_get_window(picker->ctx, picker->window);
    if (win && dw > 0.0f && dh > 0.0f) {
        float x = (dw - win->w) * 0.5f;
        float y = (dh - win->h) * 0.5f;

        n_gui_window_set_rect_axes(picker->ctx, picker->window, x < 0.0f ? 0.0f : x, y < 0.0f ? 0.0f : y, win->w, win->h, 0, 0);
    }
    n_gui_open_window(picker->ctx, picker->window);
    n_gui_raise_window(picker->ctx, picker->window);
    n_gui_mark_dirty(picker->ctx);
}

void n_gui_color_picker_close(N_GUI_COLOR_PICKER* picker) {
    if (!picker) return;
    n_gui_close_window(picker->ctx, picker->window);
    /* nobody is told about a colour nobody is editing any more */
    picker->on_change = NULL;
    picker->user_data = NULL;
    n_gui_mark_dirty(picker->ctx);
}

int n_gui_color_picker_is_open(const N_GUI_COLOR_PICKER* picker) {
    if (!picker) return 0;
    return n_gui_window_is_open(picker->ctx, picker->window) ? 1 : 0;
}

int n_gui_color_picker_get(const N_GUI_COLOR_PICKER* picker, float rgba[4]) {
    int c = 0;

    __n_assert(picker, return FALSE);
    __n_assert(rgba, return FALSE);
    for (c = 0; c < 4; c++) rgba[c] = picker->rgba[c];
    return TRUE;
}

int n_gui_color_picker_window(const N_GUI_COLOR_PICKER* picker) {
    return picker ? picker->window : -1;
}

int n_gui_color_picker_slider(const N_GUI_COLOR_PICKER* picker, int channel) {
    if (!picker || channel < 0 || channel > 3) return -1;
    return picker->sl[channel];
}

void n_gui_color_picker_free(N_GUI_COLOR_PICKER** picker) {
    if (!picker || !*picker) return;
    n_gui_color_picker_close(*picker);
    /* the widgets outlive the handle; none may call back into it */
    n_gui_window_set_close_callback((*picker)->ctx, (*picker)->window, NULL, NULL);
    Free(*picker);
}

#endif /* HAVE_ALLEGRO */