Skip to content

Multiple TextWrapped + SameLine #2313

@Unit2Ed

Description

@Unit2Ed

Version/Branch of Dear ImGui:

Version: 1.60
Branch: master

Back-end/Renderer/Compiler/OS

Back-ends: imgui_impl_dx11cpp

My Issue/Question:

I'm attempting to change formatting in the middle of a block of text, to support hyperlinks, emoji, colouring etc.

Rather than rely on a built-in markup system (eg #902) I determine the formatting externally and issue multiple ImGui commands to produce the text I need. This is fine when you don't want the text to wrap, but it becomes tricky if you use SameLine with wrapping because each subsequent command attempts to squeeze into the remaining space at the edge of the window, which grows smaller and smaller.

So far I've got it working by using CalcWrapWidthForPos/CalcWordWrapPositionA/TextUnformatted, and it works quite well. I've made a slight modification to CalcWordWrapPositionA - passing in a line_start parameter that sets the line_width local for the start position of only the first line (eg start the first line at 500, and wrap at 600, causing the second line to start back at 0). The screenshot below shows this in action, and what an equivalent sequence of PushWrapPos+TextUnformatted+SameLine commands looks like at the moment.

The code I've written to do this is quite verbose; it would be possible to wrap it up, but I think there's the possibility of integrating something like this line_start parameter deeper into ImGui so that it can do it natively.

Do you think integrating this concept deeper into ImGui is a good idea?
Do you have a better idea of how to accomplish this complex text formatting?

I'd appreciate any input you've got on this. Thank you!

Screenshots/Video
image

Standalone, minimal, complete and verifiable example: (see #2261)

// This is my modified copy of CalcWordWrapPositionA:
const char* ImFont::CalcWordWrapPositionA(float scale, const char* text, const char* text_end, float wrap_width, float line_start) const // CHANGE: New line_start parameter
{
    // Simple word-wrapping for English, not full-featured. Please submit failing cases!
    // FIXME: Much possible improvements (don't cut things like "word !", "word!!!" but cut within "word,,,,", more sensible support for punctuations, support for Unicode punctuations, etc.)

    // For references, possible wrap point marked with ^
    //  "aaa bbb, ccc,ddd. eee   fff. ggg!"
    //      ^    ^    ^   ^   ^__    ^    ^

    // List of hardcoded separators: .,;!?'"

    // Skip extra blanks after a line returns (that includes not counting them in width computation)
    // e.g. "Hello    world" --> "Hello" "World"

    // Cut words that cannot possibly fit within one line.
    // e.g.: "The tropical fish" with ~5 characters worth of width --> "The tr" "opical" "fish"

    float line_width = line_start / scale; // CHANGE: This used to start at 0
    float word_width = 0.0f;
    float blank_width = 0.0f;
    wrap_width /= scale; // We work with unscaled widths to avoid scaling every characters

    const char* word_end = text;
    const char* prev_word_end = line_start > 0.0f ? word_end : NULL;
    bool inside_word = true;

    const char* s = text;
    while (s < text_end)
    {
        unsigned int c = (unsigned int)*s;
        const char* next_s;
        if (c < 0x80)
            next_s = s + 1;
        else
            next_s = s + ImTextCharFromUtf8(&c, s, text_end);
        if (c == 0)
            break;

        if (c < 32)
        {
            if (c == '\n')
            {
                line_width = word_width = blank_width = 0.0f;
                inside_word = true;
                s = next_s;
                continue;
            }
            if (c == '\r')
            {
                s = next_s;
                continue;
            }
        }

        const float char_width = ((int)c < IndexAdvanceX.Size ? IndexAdvanceX[(int)c] : FallbackAdvanceX);
        if (ImCharIsSpace(c))
        {
            if (inside_word)
            {
                line_width += blank_width;
                blank_width = 0.0f;
                word_end = s;
            }
            blank_width += char_width;
            inside_word = false;
        }
        else
        {
            word_width += char_width;
            if (inside_word)
            {
                word_end = next_s;
            }
            else
            {
                prev_word_end = word_end;
                line_width += word_width + blank_width;
                word_width = blank_width = 0.0f;
            }

            // Allow wrapping after punctuation.
            inside_word = !(c == '.' || c == ',' || c == ';' || c == '!' || c == '?' || c == '\"');
        }

        // We ignore blank width at the end of the line (they can be skipped)
        if (line_width + word_width >= wrap_width)
        {
            // Words that cannot possibly fit within an entire line will be cut anywhere.
            if (word_width < wrap_width)
                s = prev_word_end ? prev_word_end : word_end;
            break;
        }

        s = next_s;
    }

    return s;
}


// And this is my test case
if (ImGui::Begin("Text Test", nullptr, ImGuiWindowFlags_NoTitleBar))
{
	struct Segment
	{
		Segment(const char* text, bool underline = false)
			: textStart(text)
			, textEnd(text + strlen(text))
			, colour(colour)
			, underline(underline)
		{}

		const char* textStart;
		const char* textEnd;
		ImColor		colour;
		bool		underline;
	};

	Segment segs[] = { Segment("this is a really super duper long segment that should wrap all on its own "), Segment("http://google.com", ImColor(127,127,255,255), true), Segment(" Short text "), Segment("http://github.com", ImColor(127,127,255,255), true) };

	ImGui::TextColored(ImColor(0, 255, 0, 255), "Half-manual wrapping");

	const float wrapWidth = ImGui::GetWindowContentRegionWidth();
	for (int i = 0; i < sizeof(segs) / sizeof(segs[0]); ++i)
	{
		const char* textStart = segs[i].textStart;
		const char* textEnd = segs[i].textEnd ? segs[i].textEnd : textStart + strlen(textStart);

		ImFont* Font = ImGui::GetFont();

		do
		{
			float widthRemaining = ImGui::CalcWrapWidthForPos(ImGui::GetCursorScreenPos(), 0.0f);
			const char* drawEnd = Font->CalcWordWrapPositionA(1.0f, textStart, textEnd, wrapWidth, wrapWidth - widthRemaining);
			if (textStart == drawEnd)
			{
				ImGui::NewLine();
				drawEnd = Font->CalcWordWrapPositionA(1.0f, textStart, textEnd, wrapWidth, wrapWidth - widthRemaining);
			}

			ImGui::PushStyleColor(ImGuiCol_Text, (ImU32)segs[i].colour);
			ImGui::TextUnformatted(textStart, textStart==drawEnd ? nullptr : drawEnd);
			ImGui::PopStyleColor();
			if (segs[i].underline)
			{
				ImVec2 lineEnd = ImGui::GetItemRectMax();
				ImVec2 lineStart = lineEnd;
				lineStart.x = ImGui::GetItemRectMin().x;
				ImGui::GetWindowDrawList()->AddLine(lineStart, lineEnd, segs[i].colour);

				if (ImGui::IsItemHovered(ImGuiHoveredFlags_RectOnly))
					ImGui::SetMouseCursor(ImGuiMouseCursor_TextInput);
			}

			if (textStart == drawEnd || drawEnd == textEnd)
			{
				ImGui::SameLine(0.0f, 0.0f);
				break;
			}

			textStart = drawEnd;

			while (textStart < textEnd)
			{
				const char c = *textStart;
				if (ImCharIsSpace(c)) { textStart++; }
				else if (c == '\n') { textStart++; break; }
				else { break; }
			}
		} while (true);
	}

	ImGui::NewLine();

	ImGui::Separator();
	ImGui::TextColored(ImColor(0, 255, 0, 255), "Broken native wrapping");

	ImGui::PushTextWrapPos(ImGui::GetContentRegionAvailWidth());
	for (int i = 0; i < sizeof(segs) / sizeof(segs[0]); ++i)
	{
		ImGui::TextUnformatted(segs[i].textStart, segs[i].textEnd);
		ImGui::SameLine();
	}
	ImGui::PopTextWrapPos();
}
ImGui::End();

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions