Immediate-Mode-UI / Nuklear

A single-header ANSI C immediate mode cross-platform GUI library
https://immediate-mode-ui.github.io/Nuklear/doc/index.html
8.89k stars 533 forks source link

[New feature] Add ability to change text color/background color via (optional) inline code; should be backward compatible #560

Open xzn opened 1 year ago

xzn commented 1 year ago

Originally requested here: https://github.com/vurtun/nuklear/issues/575

I happen to need this feature in my app so this is an implementation of it.

How it works

A new property draw_config is stored in nk_context and passed to nk_command_buffer when windows are drawn. nk_draw_text then use the settings in the command buffer to draw text as required.

There are two ways to encode inline color information:

1.

nk_draw_set_color_inline(ctx, NK_COLOR_INLINE_TAG);
/* then pass text to widget like follows: */
nk_label(ctx, "default [color=#ff0000]red[/color] color", NK_TEXT_LEFT)

2.

nk_draw_set_color_inline(ctx, NK_COLOR_INLINE_ESCAPE_TAG);
/* then pass text to widget like follows: */
nk_label(ctx, "default " NK_ESC "[color=#ff0000]red" NK_ESC "[/color] color", NK_TEXT_LEFT)

The difference is that in the first option, NK_ESC can be used to escape and display the opening tag without changing the color of subsequent text, e.g:

"displaying color tag " NK_ESC "[color=#ff00ff]code"

In the second option NK_ESC must be used before each opening and closing tag for them to take effect.

You can also give names to colors like so:

const char *color_name = "my_color";
struct nk_color color= nk_rgb_hex("bb00bb");
struct nk_map_name_color color_name_map;
nk_map_name_color_init_colors(&color_name_map, &ctx->memory.pool, &color_name, &color, 1);

nk_draw_push_map_name_color(ctx, &color_name_map);

nk_draw_set_color_inline(ctx, NK_COLOR_INLINE_TAG);
nk_label(ctx, "default [color=\"my_color\"]some[/color] color", NK_TEXT_LEFT)

Notes on possible improvement in code

Most of the changes in this file should not be needed, most of it can be cut down if we say, include stb_ds.h and use the hash map implementation for user-defined color name to color map. Alternatively we can choose to not support user-define color names (although it's already implemented in this PR).

Testing done so far

This PR has only been moderately tested. There should not be any buffer overrun or off by one error. If you've found any bugs please let me know so I can fix them.

Only the built-in stb_truetype font renderer has been tested. No idea right now how this feature will fare against different font rendering backend.

TODO?

Limitation in code

Tags can be nested up to only 16 levels before they have no effect, e.g.:

"[color=#ff0000]red [color=#0000ff]blue ...[/color][/color]"

In general there's no need to nest color tag to begin with unless you are generating text programmatically in some different way. Also the nested color tags are kept track of with variables on the stack so I did not include a macro define that can be used to change the limit. Nesting levels can now be changed by defining NK_INLINE_TAG_STACK_SIZE (it's still on allocated on the stack though so don't set it too large).

Example usages:

1.

/* Uncomment and change the following line if you will have deeply-nested ctx->draw_config.color_inline changes
/* #define NK_COLOR_INLINE_STACK_SIZE 32 */
nk_draw_push_color_inline(ctx, NK_COLOR_INLINE_TAG);
/* do you color coded text here */
nk_draw_pop_color_inline(ctx);

2.

nk_draw_push_map_name_color(ctx, &map_name_color);
/* do your color coded text using names for colors here (with color_inline set/pushed) */
/* useful if you want your colors to be themed and not have to re-encode the text each time */
/* nesting level allowed can be changed with NK_MAP_NAME_COLOR_STACK_SIZE */
nk_draw_pop_map_name_color(ctx);

More will be added later.

Screenshots

Will be added later.

YgorVasilenko commented 1 year ago

Really great feature! I also would like to see it merged. For now, I will have to integrate this into my own, already modified version of Nuklear. Having this merged into master would make life easier!

xzn commented 1 year ago

Hi @YgorVasilenko thanks for the kind words!

Please let me know of any issues you may have while trying to use this feature.

I'm looking for feedback on potential issues in the implementation if you could:

There are likely other concerns with the PR but these are of my own for now.

YgorVasilenko commented 1 year ago

Hi, first issue I faced: MSVC compiler turns '\e' into same ASCII as 'e' (101). This resulted in 'e' being stripped out from any colored text completely. I commented out branches of code, that do something, if symbol is '\e', and that helped.
Now I see that coloring of single string seems to be working
image

So far no memory issues.
Btw, this is where I integrated your feature, so this could be used as testing platform (it's UI editor)
https://github.com/YgorVasilenko/IndieGoUI/tree/elven_city_simulator

Ryder17z commented 1 year ago

As \ is a special character, you might have to escape it like this: \\e

xzn commented 12 months ago

Sorry my bad for not realizing that \e is not portable C and thus not working on MSVC. Maybe change it to \033 assuming we are sticking to ASCII/UTF-8?

nyaruku commented 11 months ago

Seems working well, i like this and solved my problem of having multiple texts in different colors in a horizontal row,

nyaruku commented 11 months ago

\e yep this should be also fixed, currently trying to outcomment all /e stuff too

xzn commented 11 months ago

There's a bug that wrapped text aren't currently handled. Might take a while

Fixed (hopefully)

Seems to be working: image

xzn commented 11 months ago

Updated first post to reflect the changes. A macro NK_ESC is now defined to be "\033", with NK_ESC_CHAR pointing to the same char. Can be used in string literal for concatenation.

xzn commented 11 months ago
Old comment Currently edit box can be styled this way as well, leading to cursor being in the wrong place. Should it be changed so that edit boxes are always wrapped in between ``` nk_draw_push_color_inline(ctx, NK_COLOR_INLINE_NONE); ... nk_draw_pop_color_inline(ctx); ``` Or should edit box supports this way of styling? The latter is more complicated..

Updated commits to not do inline color with edit buffer.

YgorVasilenko commented 11 months ago

Thanks a lot for wrapping text fix, we'll use that!

nyaruku commented 10 months ago

image C++ Function to create a gradient colored text

nyaruku commented 10 months ago

I use c++ to get a gradient color output:

// C
const char* custom_strcat(size_t numStrings, ...) {
    // Initialize va_list and iterate through the strings to calculate the total length
    va_list args;
    va_start(args, numStrings);

    size_t totalLen = 0;
    for (size_t i = 0; i < numStrings; i++) {
        const char* currentString = va_arg(args, const char*);
        totalLen += strlen(currentString);
    }

    va_end(args);

    // Allocate memory for the result string
    char* result = (char*)malloc(totalLen + 1); // +1 for the null terminator

    if (result == NULL) {
        return NULL; // Memory allocation failed
    }

    // Copy the contents of each string to the result
    char* currentPos = result;
    va_start(args, numStrings);

    for (size_t i = 0; i < numStrings; i++) {
        const char* currentString = va_arg(args, const char*);
        size_t currentLen = strlen(currentString);
        memcpy(currentPos, currentString, currentLen);
        currentPos += currentLen;
    }

    va_end(args);

    // Null-terminate the result string
    *currentPos = '\0';

    return result;
}

// You have to decleare an extra variable for gradientText()
// If u nest it into custom_strcat() u will still get memory leaked by gradientText()

const char* grText = gradientText("Nyaruku", "#7C7CFF", "#FF016F");
const char* labelText = custom_strcat(2, "My Discord: @", grText);

nk_label(ctx, labelText, 17);

// Prevent Memory Leak
free((void*)grText);
free((void*)labelText);

// C++
struct Color {
    int r, g, b;

    Color(int red, int green, int blue) : r(red), g(green), b(blue) {}

    // Linear interpolation between two colors
    static Color interpolate(const Color& start, const Color& end, double t) {
        int r = static_cast<int>((1 - t) * start.r + t * end.r);
        int g = static_cast<int>((1 - t) * start.g + t * end.g);
        int b = static_cast<int>((1 - t) * start.b + t * end.b);

        return Color(r, g, b);
    }
};

Color getColorForStep(const Color& start, const Color& end, int steps, int step) {
    if (step < 0) step = 0;
    if (step > steps) step = steps;

    double t = static_cast<double>(step) / steps;
    return Color::interpolate(start, end, t);
}

Color HexColorToRGB(const char* hexColor) {
    Color color{255,255,255};
    if (hexColor[0] == '#' && strlen(hexColor) == 7) {
        std::stringstream ss;
        ss << std::hex << hexColor + 1; // Skip the '#' character

        int colorValue;
        ss >> colorValue;
        color.r = (colorValue >> 16) & 0xFF;
        color.g = (colorValue >> 8) & 0xFF;
        color.b = colorValue & 0xFF;
        ss.clear();
    }
    else {
        // Invalid hex color format, return the original color
    }

    return color;
}
std::string RGBColorToHex(Color color) {
    std::stringstream ss;
    ss << "#" << std::setfill('0') << std::setw(2) << std::hex << color.r
        << std::setw(2) << std::hex << color.g << std::setw(2) << std::hex << color.b;
    std::string temp = ss.str();
    ss.clear();
    return temp;
}

const char* gradientText(const char* text, const char* color_start, const char* color_end) {
    std::stringstream result;
    Color color1 = HexColorToRGB(color_start);
    Color color2 = HexColorToRGB(color_end);

    int totalSteps = strlen(text);
    for (int i = 0; i < totalSteps; i++) {
        result << "[color=" << RGBColorToHex(getColorForStep(color1, color2, totalSteps, i)) << "]" << text[i] << "[/color]";
    }

    std::string resultStr = result.str();
    char* cString = new char[resultStr.size() + 1];
    strcpy(cString, resultStr.c_str());

    return cString;
}
yukyduky commented 10 months ago

image I had to cast the memory in 5 places before I got it to compile since I'm coding in C++, might be something worth adding before pulling this in. Issue is on line 8380, 9964, 8192, 8220, 8335. I'm going to play around with the code now, but it looks promising!

EDIT: The text doesn't wrap anymore if you make the window scalable and drag it.

xzn commented 10 months ago

@yukyduky I've added the casts to the commits to help C++ compat.

For the wrapping text, did you update the container size for the wrapped text when you resize the window? Does it not work properly even when you do that?

Edit: seems to work with the updated sdl_opengl3 demo (changed width of scalable area to be a factor of the containing window instead of being fixed):

https://github.com/Immediate-Mode-UI/Nuklear/assets/1617680/bd7efd52-ab55-4694-8dcc-96015744daa4

Edit 2:

Looks like I didn't read through the docs properly, so dynamic layout is a thing haha ( ty @yukyduky ).

yukyduky commented 10 months ago

@xzn Nice! Thanks for the quick update :)

One of the benefits is and should be to be able to resize the windows on the fly while the app is running, imo. It's possible to do so in the current version so can't be too hard to get it working again.

yukyduky commented 10 months ago

@xzn Ahh this is my fault, I've pretty much only used the nk_layout_dynamic_row and didn't think about that I was using your window with the static layout to test your code. In that case I have no further issues, sorry for the confusion^^

IgorAlexey commented 7 months ago

Why is this not merged?

mhcerri commented 6 months ago

Any plans to merge this feature?