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

Change color within text / colored text #902

Open
JLFSL opened this issue Nov 11, 2016 · 45 comments
Open

Change color within text / colored text #902

JLFSL opened this issue Nov 11, 2016 · 45 comments

Comments

@JLFSL
Copy link

JLFSL commented Nov 11, 2016

Hi there, I was wondering if there was any way to change from color while still using the ::Text function, e.g:
ImGui::Text("Please fill in your #FF0000 to #0000FFlogin.");

Thanks.

@ocornut
Copy link
Owner

ocornut commented Nov 11, 2016

You'd need to write a custom function that does some sort of mark-down-ish parsing and render as it goes.
We don't have one, when we have one it'll most probably be a separate function.

@ocornut
Copy link
Owner

ocornut commented Nov 11, 2016

I have a simple one I made for my own use on a project but it's not really fit for use elsewhere.

You can grab here but it'd need fixing/reworking/finishing if you want to use it in your own project.
ui_richtext.h.txt
ui_richtext.cpp.txt

@pdoane
Copy link

pdoane commented Nov 12, 2016

The difficulty with it as a separate top-level function is that many ImGui functions take text as an argument (e.g. buttons, trees). I think that ImGui needs some kind of standard escape mechanism even if the particulars are left up to the application to interpret the content.

@ocornut
Copy link
Owner

ocornut commented Nov 12, 2016

That's a good point, we could have an escape mecanism for simple stuff (such as color change?), maybe with the possibility of enabling/disabling/setting a custom one.

What sort of feature would you want to use and in what sort of context?

@ocornut
Copy link
Owner

ocornut commented Nov 12, 2016

Also worth reminding that text size calculation can be a noticeable bottleneck for large UI and therefore this has to be handled properly to not affect perfs too much.

If the marking/escaping mechanism is optional it gives more flexibility there (because we aren't concerned by performances as much anymore).

Related because closely related: the shortcut system (#456) will also needs a way to render underscore, probably tagged Windows-style where "&File" will render an underscore under the F, potentially optional (for regular buttons, modern Windows tends to hide the underscore using ALT is held).
That probably means the system would need to accommodate for optional features.

@ratchetfreak
Copy link

About text size calc maybe worth doing some caching. Most of the strings being rendered will be fixed and be rendered again every frame.

@ocornut
Copy link
Owner

ocornut commented Nov 12, 2016

Would be worth it.

  • Font, font size, text wrapping (+pos) needs to be included in key.
  • A highly populated map with lots of insertion needs better code than current key-value map we use (which isn't insertion-friendly and doesn't clean up).
  • Hashing would lead to a worst worse case (maybe not a problem?)

So that's some work and a different feature that discussed here. Something to keep in mind.

For now implementing the marking/escaping would go first.

@ocornut
Copy link
Owner

ocornut commented Nov 12, 2016

Note that current text size computation is already fairly lightweight (inner loop pulling from a hot-only dense array of XAdvance values), so replacing it with Hash + Lookup isn't going to be trivial massive win.

@pdoane
Copy link

pdoane commented Nov 14, 2016

What sort of feature would you want to use and in what sort of context?

The main one I want has been highlighting a search substring in lists/trees. The escape mechanism feels like it fits in the best with ImGui design because so many entrypoints take strings as parameters.

@ocornut
Copy link
Owner

ocornut commented Nov 15, 2016

Possible features:

  • Change colors using values encoded in string e.g. [col=FF00000]test[/col]
  • Change colors using high-level semantic e.g. [b]test[/b] and we have a color for Text and a color for TestHighlight. The same way we currently have TextDisabled. I feel this would be simplest and a rather sane thing to do.
  • Set a background color (essentially filling behind characters).
  • Enable/disable underline (would that be an hardcoded concept in term of drawing?)
  • Changing font (to e.g. get actual bold/italic). Feels a bit hairy to get right

And what would be the syntax? Feels like using [] would collide too much with normal strings and lead to parsing problems, so \ would be a better start and we can have a push/pop system to enable/disable escaping.

@pdoane
Copy link

pdoane commented Nov 15, 2016

ANSI escape codes cover that feature set so that's one possibility.

If some other syntax is used, I'd prefer some non-printable character so that all existing strings continue to work without change.

@JLFSL
Copy link
Author

JLFSL commented Nov 15, 2016

Having something similar to BBCode would be amazing.

@pdoane
Copy link

pdoane commented Nov 16, 2016

Having something similar to BBCode would be amazing.

Are you suggesting you would want it applied pervasively or as a separate entrypoint? I'd be concerned if something like BBCode/Markdown/HTML were applied at the lowest level of string formatting as escaping source data is a lot more annoying/expensive with an immediate mode API.

@ratzlaff
Copy link

since I am familiar with ANSI escape codes, that one gets my vote
https://conemu.github.io/en/AnsiEscapeCodes.html#SGR_Select_Graphic_Rendition_parameters

@vivienneanthony
Copy link

I am curious. Is there any update about this topic?

@ocornut
Copy link
Owner

ocornut commented Jan 1, 2017

None. Someone may want to get ahead toward a proof of concept. I know it'll take me too much time (several full days) to do it totally properly and with perf/options I'm happy with, so I'm not looking into it at all now.

@ocornut
Copy link
Owner

ocornut commented Jan 19, 2017

Note that #986 is suggesting using a similar feature during text edition.

i just need word coloring in the text multi line control.

I hope i can have a string with information about word start, word end, and word color to pass to the multi-line control. like that maybe "start : end : color" => "10:22:#99ccff"

So before the multi-line control i imagine analyse the text and format a special string for it in the ImGuiTextEditCallback

Finally i imagine pass this buffer to a modified version of ImFont::RenderText.

Do you think i may be heavy , maybe you have another way ?

Thanks for your help

I don't think the suggested solution would be appropriate, but we can keep in mind and consider how coloring might work in text edition in the future.

@aiekick
Copy link
Contributor

aiekick commented Jan 19, 2017

Finally for the moment i use a special buffer who have the same length as the buffer of the text multi line control. and each char corresponding to a color settings in an array in ImGuiStyle.

struct ImGuiStyle { ImVec4 SyntaxColors[255]; }

So i have 254 possibility of coloration.

My function analyse the buffer by the callback when i edit some things and format the color of words in the special buffer passed to the multi-line control.

and in void ImFont::RenderText, i do that :

the buffer is a 'const char* syncol'

ImGuiStyle& style = ImGui::GetStyle();

ImU32 defaultCol = col;

    while (s < text_end)
    {
    	if (syncol != 0)
    	{
    		col = defaultCol;
    		char c = *(syncol + (s - text_begin));
    		if (c != '\0')
    			col = ImColor(style.SyntaxColors[c]);
    	}

It work like a charm. so now i need to write a good syntax coloring func for my needs :)

screenhunter_284

@David20321
Copy link

David20321 commented Feb 1, 2017

In Overgrowth I added the ##FFFFFFFF style coloring like this: https://gist.github.com/David20321/9e349d197b19e4919614652e4c0d175b

Doesn't work for editable text, just for display.

@bluebear94
Copy link

Actually, I was going to implement this myself with my own copy. ;)

Perhaps along with embedding icons inside text, but I doubt that would ever be added.

@ocornut
Copy link
Owner

ocornut commented Feb 2, 2017

@David20321

In Overgrowth

Your game looks amazing! Would be nice if you could post some shots showcasing your use of imgui in the screenshots threads sometime.

@bluebear94

Perhaps along with embedding icons inside text, but I doubt that would ever be added.

Merging an icon font such as FontAwesome into the main font is very easy and very very useful. Read the documentation in extra_font. I am using https://github.com/juliettef/IconFontCppHeaders as a helper to get #define for the various icons.

@bluebear94
Copy link

@ocornut That's possible, but then you can't have icons using multiple colours, can you? But then, I'm not sure why anyone would want full-colour icons outside of development tools.

@ocornut
Copy link
Owner

ocornut commented Feb 7, 2017

@bluebear94 You can't indeed. Give it a try someday, using FontAwesome merged within ImGui default font, it's really super convenient. Considering adding it to the demo app.

@chadlyb
Copy link

chadlyb commented Mar 21, 2017

I have a bit of a hack I'm using that works alright for my purposes (a read-only multiline buffer.) Not industrial-grade, but here goes:

void ImFont::RenderText(ImDrawList* draw_list, float size, ImVec2 pos, ImU32 col, const ImVec4& clip_rect, const char* text_begin, const char* text_end, float wrap_width, bool cpu_fine_clip) const
{
	const ImU32 originalCol = col;

	...
      		if (c == '\r')
            		continue;
	}

	if (c >= 0x100000 && c <= 0x10FFFD )
	{
		if ( c == 0x108000 )
		{
			col = originalCol;
		}
		else
		{
			col =   0xFF000000
				  + ((c >> 8) & 0x0F) * 0x00000011
				  + ((c >> 4) & 0x0F) * 0x00001100
				  + ((c     ) & 0x0F) * 0x00110000;
		}
		continue;
	}

	float char_width = 0.0f;
	if (const Glyph* glyph = FindGlyph((unsigned short)c))
	{
	...

Then, use Unicode 0x100rgb to represent your color (0x108000 resets to default color.)
Seems to work well enough for read-only buffers, including cursor navigation and selection, for my purposes.

@ldecarufel
Copy link

ldecarufel commented Apr 3, 2017

Hi there! I just wanted to share my simple solution to embed colors in ImGui text.

I basically parse the string while accumulating characters and looking for inline color blocks. When found I extract the color, then I simply use ImGui::Text() and ImGui::SameLine(0,0) to print the accumulated text, I push the new color using ImGui::PushStyleColor(), and I start to accumulate text again. I also use ImGui::PopStyleColor() before pushing a new color if one was pushed before.

The code supports 6-char colors (RGB), and 8-char colors (ARGB). Using an empty color block simply returns to the default color.

You can find the code here on PasteBin.

Here's how it's used, using "{" and "}" to mark colors:

const float colPos = 220.0f;
ImGui::TextWithColors( "{77ff77}(A)" ); ImGui::SameLine( colPos ); ImGui::Text( "Press buttons, toggle items" );
ImGui::TextWithColors( "{77ff77}(A){} + DPad Left/Right" ); ImGui::SameLine( colPos ); ImGui::Text( "Manipulate items" );
ImGui::TextWithColors( "{ff7777}(B)" ); ImGui::SameLine( colPos ); ImGui::Text( "Close popups, go to parent, clear focus" );
ImGui::TextWithColors( "{7777ff}(X)" ); ImGui::SameLine( colPos ); ImGui::Text( "Access menus" );

That's a pretty simple solution that works only for regular text, but that's all we really needed for now.

@styves
Copy link

styves commented Jul 20, 2017

I made a simple edit to RenderText to support Quake style color codes. It's rather simple, ^ is used as an escape followed by an index into the color code list.

void ImFont::RenderText(ImDrawList* draw_list, float size, ImVec2 pos, ImU32 col, const ImVec4& clip_rect, const char* text_begin, const char* text_end, float wrap_width, bool cpu_fine_clip) const
{
    const ImU32 alpha = (col >> 24);
    const ImU32 color_codes[9] =
    {
        col,                           // default 0
        ImColor(255,   0,   0, alpha), // red     1
        ImColor(  0, 255,   0, alpha), // green   2
        ImColor(255, 255,   0, alpha), // yellow  3
        ImColor(  0,   0, 255, alpha), // blue    4
        ImColor(  0, 255, 255, alpha), // cyan    5
        ImColor(255,   0, 255, alpha), // magenta 6
        ImColor(255, 255, 255, alpha), // white   7
        ImColor(  0,   0,   0, alpha), // black   8
    };
    ...
    while (s < text_end)
    {
        if (*s == '^' && *(s + 1) && *(s + 1) != '^')
        {
            col = color_codes[(*(s + 1) - '0') % 8];
            s += 2;
            continue;
        }

        if (word_wrap_enabled)
        {
        ...
                y += line_height;
                col = color_codes[0]; // reset color code on new line

                if (y > clip_rect.w)
                ...

There's unfortunately some cursor funkiness in my input fields, I'll update the comment when I get around to fixing it.

Edit: Here we go, changing InputTextCalcTextSizeW seems to have worked out. If anyone thinks there's a better way I'm all ears.

static ImVec2 InputTextCalcTextSizeW(const ImWchar* text_begin, const ImWchar* text_end, const ImWchar** remaining, ImVec2* out_offset, bool stop_on_new_line)
{
...
        if (c == '\r')
            continue;
        if (c == '^' && *(s) && *(s) != '^')
        {
            ++s;
            continue;
        }
        const float char_width = font->GetCharAdvance((unsigned short)c) * scale;
        ...

@ddovod
Copy link

ddovod commented Jan 2, 2019

https://gist.github.com/ddovod/be210315f285becc6b0e455b775286e1
Here's a gist with ImGui::TextAnsiColored function. Kinda dirty, but it works fine for me. The main part is in the ParseColor and ImFont_RenderAnsiText functions. Actually most of the code is copy-pasted from the original imgui code, just to not touch it. It supports only foreground colors (since there's no easy way to draw background color and bold/reversed/etc ansi formats). Based on this gist
https://gist.github.com/David20321/9e349d197b19e4919614652e4c0d175b

Looks like this
ansicolors

@ocornut
Copy link
Owner

ocornut commented Jan 31, 2019

While this doesn't cancel/replace the need for a low-level, general mechanism to colorize text, I should mention that @juliettef today released an imgui_markdown helper which would probably satisfy the related need of showing less bland chunk of information:

https://github.com/juliettef/imgui_markdown

imgui_markdown_demo_live_editing

I added it to the wiki and this is also a reminder that however humble there is a wiki gathering links.

Also from this specific tool, I would like to later expose new text colors in ImGuiStyle, namely to denote "highlight/bold" or "link" colors. This is so whatever color changing mechanism we expose should be able to use generic semantic such as "highlight/bold" instead of refering to hardcoded RGB colors. (Of course hardcoded RGB should still be possible, but we want to maximize the style-portability of common uses).

@fungos
Copy link

fungos commented Apr 17, 2019

I did a very simple extension implementation for a callback based custom tags processing here (there is more than rich text):
https://github.com/ocornut/imgui/compare/docking...fungos:richtitlebar?expand=1
or here with an older but cleaner diff: fungos@62fe0cf

With this, one call like:

if (ImGui::BeginTitleMenuBar("{icon.png} App Main Window Custom Title Bar - {healthbar} - "))

results in this:

sample

This requires two callbacks to be implemented by the user, one wich calculates the area to be drawn for some tag, and another that do the drawing itself. The implementation from this sample is here.

@connorjak
Copy link

https://gist.github.com/ddovod/be210315f285becc6b0e455b775286e1
Here's a gist with ImGui::TextAnsiColored function. Kinda dirty, but it works fine for me. The main part is in the ParseColor and ImFont_RenderAnsiText functions. Actually most of the code is copy-pasted from the original imgui code, just to not touch it. It supports only foreground colors (since there's no easy way to draw background color and bold/reversed/etc ansi formats). Based on this gist
https://gist.github.com/David20321/9e349d197b19e4919614652e4c0d175b

Looks like this
ansicolors

@ddovod I have made a port of this functionality on a fork of pyimgui: https://github.com/connorjak/pyimgui/tree/AnsiText

BruceCherniak pushed a commit to RenderKit/ospray-studio that referenced this issue Aug 9, 2021
I wish we could have inline color commands to make the type darker, but
that doesn't exist (yet) without custom solutions

ocornut/imgui#902
@ocornut ocornut mentioned this issue Apr 19, 2022
@TYSON-Alii
Copy link

Colorful text.

Type the character you have specified for the color you want after the $ sign.

Example:

static const auto& code = R"($r#include $y<iostream>
$busing namespace $wstd$l; $d// for easy

$bauto $wmain$l() -> $bint $l{
    $pif $l($o2 $l+ $o2 $l== $o4$l)
        $wcout $l<< $f"Hello, World." $l<< $m'\n'$l;
    $preturn $o0$l;
};
)";
static const ImVec4&
    white = { 1,1,1,1 },
    blue = { 0.000f, 0.703f, 0.917f,1 },
    red = { 0.976f, 0.117f, 0.265f ,1 },
    grey = { 0.230f, 0.226f, 0.289f,1 },
    lgrey = { 0.630f, 0.626f, 0.689f,1 },
    green = { 0.000f, 0.386f, 0.265f,1 },
    lime = { 0.55f, 0.90f, 0.06f,1 },
    yellow = { 0.91f, 1.00f, 0.21f,1 },
    purple = { 1,0,1,1 },
    orange = { 1.00f, 0.36f, 0.09f,1 };
ColorfulText(code, {
    {'w', white},
    {'b', blue},
    {'d', grey},
    {'l', lgrey},
    {'f', green},
    {'m', lime},
    {'y', yellow},
    {'p', purple},
    {'r', red},
    {'o', orange}
});

image
another ss
unknown

Souce Code

using str = std::string;
template <typename T>
using list = std::pmr::vector<T>;
namespace im = ImGui;
void ColorfulText(const str& text, const list<pair<char, ImVec4>>& colors = {}) {
    auto p = im::GetCursorScreenPos();
    const auto first_px = p.x, first_py = p.y;
    auto im_colors = ImGui::GetStyle().Colors;
    const auto default_color = im_colors[ImGuiCol_Text];
    str temp_str;
    struct text_t {
        ImVec4 color;
        str text;
    };
    list<text_t> texts;
    bool color_time = false;
    ImVec4 last_color = default_color;
    for (const auto& i : text) {
        if (color_time) {
            const auto& f = std::find_if(colors.begin(), colors.end(), [i](const auto& v) { return v.first == i; });
            if (f != colors.end())
                last_color = f->second;
            else
                temp_str += i;
            color_time = false;
            continue;
        };
        switch (i) {
        case '$':
            color_time = true;
            if (!temp_str.empty()) {
                texts.push_back({ last_color, temp_str });
                temp_str.clear();
            };
            break;
        default:
            temp_str += i;
        };
    };
    if (!temp_str.empty()) {
        texts.push_back({ last_color, temp_str });
        temp_str.clear();
    };
    float max_x = p.x;
    for (const auto& i : texts) {
        im_colors[ImGuiCol_Text] = i.color;
        list<str> lines;
        temp_str.clear();
        for (const auto& lc : i.text) {
            if (lc == '\n') {
                lines.push_back(temp_str += lc);
                temp_str.clear();
            }
            else
                temp_str += lc;
        };
        bool last_is_line = false;
        if (!temp_str.empty())
            lines.push_back(temp_str);
        else
            last_is_line = true;
        float last_px = 0.f;
        for (const auto& j : lines) {
            im::RenderText(p, j.c_str());
            p.y += 15.f;
            last_px = p.x;
            max_x = (max_x < last_px) ? last_px : max_x;
            p.x = first_px;
        };
        const auto& last = lines.back();
        if (last.back() != '\n')
            p.x = last_px;
        else
            p.x = first_px;
        if (!last_is_line)
            p.y -= 15.f;
        if (i.text.back() != '\n')
            p.x += im::CalcTextSize(last.c_str()).x;
    };
    im_colors[ImGuiCol_Text] = default_color;
    im::Dummy({ max_x - p.x, p.y - first_py });
};

@ForrestFeng
Copy link

ForrestFeng commented May 4, 2022

Change color within text / colored text

I have enhanced the ImGui::TextUnformated() method to make it support arbitrary text color, underline, strikethrough, highlight, mask

I aslo add one demo for this feature.

image

The code for the demo is shown below.

The new TextUnformatted signature was changed to support this feature. The wrapped, disabled, and customization are newly added args with default values. So it should be compatible with existing code.

IMGUI_API void TextUnformatted(const char* text, const char* text_end = NULL, bool wrapped = false, bool disabled = false, const ImTextCustomization *customization = NULL);

       IMGUI_DEMO_MARKER("Widgets/Text/Text Customization");
        if (ImGui::TreeNode("Text Customization Simple"))
        {
            ImColor red(255, 0, 0, 255);
            ImColor green(0, 255, 0, 255);
            ImColor blue(0, 0, 255, 255);
            ImColor yellow(255, 255, 0, 255); 
            ImColor brown(187, 126, 0, 255);
            ImColor cyan(0, 255, 255, 255);
            ImColor magenta(255, 0, 255, 125);

            const char* text = "The quick red fox jumps over the green box.";
            ImTextCustomization tc;

            ImGui::NewLine();
            ImGui::Text("Color the whole text");            
            ImGui::TextUnformatted(text, NULL, true, false, &tc.Range(text).TextColor(green));

            ImGui::NewLine();
            ImGui::Text("Color the sustring of the text");
            ImGui::TextUnformatted(text, NULL, true, false, &tc.Clear().Range(text + 14, text + 17).TextColor(red).Range(text + 39, text + 42).TextColor(green));

            ImGui::NewLine();
            ImGui::Text("Underline");
            ImGui::TextUnformatted(text, NULL, true, false, &tc.Clear().Range(text).Unerline());

            ImGui::NewLine();
            ImGui::Text("Underline with color");
            ImGui::TextUnformatted(text, NULL, true, false, &tc.Clear().Range(text).Unerline(blue));

            ImGui::NewLine();
            ImGui::Text("Strikethrough");
            ImGui::TextUnformatted(text, NULL, true, false, &tc.Clear().Range(text).Strkethrough());

            ImGui::NewLine();
            ImGui::Text("Strikethrough with color");
            ImGui::TextUnformatted(text, NULL, true, false, &tc.Clear().Range(text).Strkethrough(red));

            ImGui::NewLine();
            ImGui::Text("Hilight the text with brown");
            ImGui::TextUnformatted(text, NULL, true, false, &tc.Clear().Range(text).Highlight(brown));

            ImGui::NewLine();
            ImGui::Text("Mask the text so it is not readable");
            ImGui::TextUnformatted(text, NULL, true, false, &tc.Clear().Range(text+10, text+17).Mask());

            ImGui::NewLine();
            ImGui::Text("Mask the text with color");
            ImGui::TextUnformatted(text, NULL, true, false, &tc.Clear().Range(text+10, text+17).Mask(cyan));

            ImGui::NewLine();
            ImGui::Text("Mask the text with semitransparent color");
            ImGui::TextUnformatted(text, NULL, true, false, &tc.Clear().Range(text+10, text+28).Mask(magenta));
            ImGui::NewLine();

            ImGui::TreePop();
        }

Those text styles are all with a text and can be combined as needed.

Here is another demo. It allows the user to adjust the ranges to apply a style and see the result.

image

The code is on Text Customization Branch

Any comments please let me know.

If it is possible I will create a pull request when ready. Thanks.

@ForrestFeng
Copy link

ForrestFeng commented May 4, 2022

Add one more video for this feature.

ImGui.Text.Customization.Video_2022-05-04_132237.mp4

Quintus added a commit to Quintus/ilmendur that referenced this issue Dec 28, 2022
This currently maps to coloring the text red. When ImGui's ticket
ocornut/imgui#902 is resolved, one can look
into supporting additional markup. For now, this should be sufficient.
@S-A-Martin
Copy link

Still seems to be a lot of interest in this. Has support for this been pulled into the official ImGui yet?

@ocornut
Copy link
Owner

ocornut commented Jan 24, 2023

I'm a little puzzled why many people are writing messages on an open pull-request or issues asking if the PR/issue has been solved :) no it hasn't.

But I have been recently working on new low-level text functions and I am confident we can make interesting progress on this in the coming quarters (borrowing some ideas from #3130 and #5288 but with a different implementation).

@codecat
Copy link
Contributor

codecat commented Jan 24, 2023

That's awesome! Personally, I hope that any official support for this can be customized; many people already seem to have this "hacked in" with their own syntax.

For example, in my case this would be \$f00Red Text\$zNormal text, but that's very different from what's suggested in this issue.

Additionally, I also implemented text shadow using the \$s syntax, so I wonder how extensible such a system would be.

@ocornut
Copy link
Owner

ocornut commented Jan 24, 2023

I don't have answer for this yet

A few things that are certain:

  • we'll probably to be able to feed decoration/markup inside text (e.g. Button("some label with markup")) as well as externally (e.g. InputText() with custom syntax coloring provided on a separate stream).
  • things need to be ultra fast. means parsing and skipping needs to be optimal and the syntax (IF there is a single syntax) needs to be designed around this.

Uncertain but obviously desirable:

  • Ways to creatively extend this. But maybe the extension will need to follow a common syntax etc. I don't know.

@ajh123
Copy link

ajh123 commented Apr 3, 2023

Example of ANSI text rendering:

Maybe use these as an idea for text colours.

They are a bit old, so they might be incompatible with recent ImGui versions.

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

No branches or pull requests