Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wants to merge 14 commits into
base: master
Choose a base branch
from

Conversation

xzn
Copy link

@xzn xzn commented May 30, 2023

Originally requested here: vurtun/nuklear#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:

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)
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?

  • Add web colors to a struct nk_map_name_color in a file in the demo directory.
  • More testing.
  • Add documentation?
  • Update changelog when ready

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:

/* 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);
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
Copy link
Contributor

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
Copy link
Author

xzn commented Jul 19, 2023

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:

  • How easy it is to use the new functions correctly.
  • The string parsing code here is fairly ad-hoc and I'm not sure if it's actually doing the correct thing (aside from the limited testing I did).
  • Whether the ways memory management is used are correct for custom color names.
  • Whether texts are aligned properly when rendered in ranges of different colors.

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

@YgorVasilenko
Copy link
Contributor

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
Copy link
Contributor

Ryder17z commented Jul 23, 2023

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

@xzn
Copy link
Author

xzn commented Jul 23, 2023

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
Copy link
Contributor

nyaruku commented Aug 17, 2023

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

@nyaruku
Copy link
Contributor

nyaruku commented Aug 17, 2023

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

@xzn
Copy link
Author

xzn commented Aug 19, 2023

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

Fixed (hopefully)

Seems to be working:
image

@xzn
Copy link
Author

xzn commented Aug 19, 2023

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
Copy link
Author

xzn commented Aug 19, 2023

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
Copy link
Contributor

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

@nyaruku
Copy link
Contributor

nyaruku commented Sep 8, 2023

image
C++ Function to create a gradient colored text

@nyaruku
Copy link
Contributor

nyaruku commented Sep 8, 2023

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
Copy link
Contributor

yukyduky commented Sep 11, 2023

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
Copy link
Author

xzn commented Sep 11, 2023

@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):

Recording.2023-09-12.035216.mp4

Edit 2:

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

@yukyduky
Copy link
Contributor

@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
Copy link
Contributor

@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
Copy link

Why is this not merged?

@mhcerri
Copy link

mhcerri commented Jan 23, 2024

Any plans to merge this feature?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants