Skip to content

TextAlignedV(): better handling of cursor position and DC boundary - #9455

Open
dkosmari wants to merge 1 commit into
ocornut:masterfrom
dkosmari:upstream-text-aligned
Open

TextAlignedV(): better handling of cursor position and DC boundary#9455
dkosmari wants to merge 1 commit into
ocornut:masterfrom
dkosmari:upstream-text-aligned

Conversation

@dkosmari

Copy link
Copy Markdown

The current TextAlignedV() leaves the position and boundary in an inconsistent state, that limits its usefulness.

With this PR:

  • The widget always has width width (parameter was called size_x before), unless:
    • width == 0 means the real text width, as obtained by CalcTextSize().x. Before, size_x == 0 was giving me no rendered output, as if it was actually clipping the text to a zero-width region.
    • width < 0 means ContentRegionAvail().x.
  • Cursor position actually matches the right of the displayed text.
  • DC boundary is updated according to the actual displayed text.

Here's a test code (call test_text_aligned() from any example code), showing the bounding box, cursor position behavior, and how it behaves inside table cells:

void show_bounding_box()
{
    ImVec2 min = ImGui::GetItemRectMin();
    ImVec2 max = ImGui::GetItemRectMax();
    ImU32 col = ImGui::GetColorU32(ImVec4{1.0f, 0.0f, 0.0f, 0.5f});
    ImDrawList* draw_list = ImGui::GetCurrentWindow()->DrawList;
    draw_list->AddRect(min, max, col);
}

void test_text_aligned()
{
    static float width = 200.0f;
    static float align = 0.0f;
    if (ImGui::Begin("Test TextAligned()")) {

        ImGui::TextWrapped("Test cursor positioning and bounding box.");

        ImVec2 available = ImGui::GetContentRegionAvail();
        ImGui::SliderFloat("align", &align, 0.0f, 1.0f);
        ImGui::DragFloat("width", &width, 1.0f, -1.0f, available.x, "%.0f");
        ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, {0, 0});
        if (ImGui::BeginChild("container", {},
                              ImGuiChildFlags_Borders | ImGuiChildFlags_AutoResizeY,
                              ImGuiWindowFlags_HorizontalScrollbar)) {
            ImGui::AlignTextToFramePadding();
            ImGui::TextAligned(align, width, "Irish wristwatch");
            show_bounding_box();
            ImGui::SameLine();
            ImGui::Button("##test", {10, 0});

            ImGui::Button("##test2", {10, 0});
            ImGui::SameLine();
            ImGui::TextAligned(align, width, "Rear wheel drive");
            show_bounding_box();
        }
        ImGui::EndChild();
        ImGui::PopStyleVar();

        ImGui::Separator();

        ImGui::TextWrapped("Test inside a table.");

        if (ImGui::BeginTable("table", 3,
                              ImGuiTableFlags_Borders |
                              ImGuiTableFlags_Resizable)) {

            static float apple_align  = 0.0f;
            static float banana_align = 0.5f;
            static float cherry_align = 1.0f;

            ImGui::TableNextRow(ImGuiTableRowFlags_Headers);
            ImGui::TableNextColumn();
            ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
            ImGui::SliderFloat("##apple_align", &apple_align, 0.0f, 1.0f);
            ImGui::TableNextColumn();
            ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
            ImGui::SliderFloat("##banana_align", &banana_align, 0.0f, 1.0f);
            ImGui::TableNextColumn();
            ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x);
            ImGui::SliderFloat("##cherry_align", &cherry_align, 0.0f, 1.0f);

            ImGui::TableNextRow();
            ImGui::TableNextColumn();
            ImGui::TextAligned(apple_align, -1.0f, "Apple");
            ImGui::TableNextColumn();
            ImGui::TextAligned(banana_align, -1.0f, "Banana");
            ImGui::TableNextColumn();
            ImGui::TextAligned(cherry_align, -1.0f, "Cherry");

            ImGui::EndTable();
        }
    }
    ImGui::End();
}

Here are images showing different outputs:

Screenshot from 2026-06-25 21-38-16 Screenshot from 2026-06-25 21-38-30 Screenshot from 2026-06-25 21-38-44 Screenshot from 2026-06-25 21-39-30

@ocornut
ocornut force-pushed the upstream-text-aligned branch from fe3d9e4 to 26c1278 Compare August 7, 2026 14:02
@ocornut

ocornut commented Aug 7, 2026

Copy link
Copy Markdown
Owner

cc #7024
I believe there are four distinct things here.

The widget always has width width (parameter was called size_x before), unless:
width == 0 means the real text width, as obtained by CalcTextSize().x. Before, size_x == 0 was giving me no rendered output, as if it was actually clipping the text to a zero-width region.
width < 0 means ContentRegionAvail().x.

[1] If you use CalcTextSize().x as a widget width there's no reason to perform any alignment.
Can you clarify why you would want this?

The widget always has width width

[2] This seems better indeed.
I don't remember my exact reasoning but if you see this example:
#7024 (comment)
It would allow removing the code that calculate and pass right_x to SameLine().

I think my intuition then was to not claim more space than necessary in order to hypothetically be able to do a SameLine() within the remaining space. But it only really makes sense if align_x == 0.0f ?

Cursor position actually matches the right of the displayed text.

[3] Correct.

DC boundary is updated according to the actual displayed text.

[4] Which boundaries precisely? And why changing them?
The previous code was intentionally keeping CursorMaxPos.x unchanged in favor of updating IdealMaxPos.x in order to allow auto-resize to work. Your code breaks that.


I think I agree on point 2 and 3 but not on 1, and 4 needs clarifying.

@ocornut

ocornut commented Aug 7, 2026

Copy link
Copy Markdown
Owner

I have pushed 551a0a6.

It applies [2] which automatically fixes [3].
I however did not apply [1] and [4].

Let me know what you think!

@dkosmari

dkosmari commented Aug 7, 2026

Copy link
Copy Markdown
Author

[1] I don't have a use case for that, other than "keep the same semantics as Button() just in case."

[4] I think the incorrect position was not sizing the parent window correctly after ellipsizing. I'll check what the current code does.

In my fork, I went ahead and deleted the SetTooltip() call altogether. I made the function return true if the text was ellipsized, to let the user decide what to do.


That said, I'd rather have a unified Text() function that worked like this:

enum ImGui_TextFlags_ {
    ImGui_TextFlags_None       = 0, // no text processing, either show full text or clip
    ImGui_TextFlags_Wrap       = 1 << 0,
    ImGui_TextFlags_Ellipsize  = 1 << 1, // ellipsize text instead of clipping
    ImGui_TextFlags_SingleLine = 1 << 2, // don't wrap, don't show anything beyond the first '\n'
    ImGui_TextFlags_BreakAll   = 1 << 3, // same meaning as CSS word-break: break-all
    ImGui_TextFlags_Underline  = 1 << 4, // add bold and italics flags too, if the font backends support it
};

typedef int ImGui_TextFlags;

struct ImTextSpecs {
    ImVec2 size = { -1.0f, -1.0f };
    ImVec2 align = { 0.0f, 0.5f };
    ImGui_TextFlags flags = ImGui_TextFlags_None;
};

struct ImTextLayoutResult {
    ImVec2 size;     // area actually used to show the text
    bool wrapped;    // true if any text wrapping happened
    bool incomplete; // true if any part of the text was not fully rendered
};

void ImGui::Text(const ImTextSpecs& specs, const char* fmt, ...);

void ImGui::Text(const ImTextSpecs& specs, ImTextLayoutResult& layout_result, const char* fmt, ...);

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants