Skip to content

Fix TextBlock ellipsis not showing with TextWrapping and MaxLines - #22017

Merged
MrJul merged 5 commits into
AvaloniaUI:mainfrom
puk06:fix/textblock-ellipsis-maxlines
Aug 24, 2026
Merged

Fix TextBlock ellipsis not showing with TextWrapping and MaxLines#22017
MrJul merged 5 commits into
AvaloniaUI:mainfrom
puk06:fix/textblock-ellipsis-maxlines

Conversation

@puk06

@puk06 puk06 commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

What does the pull request do?

When MaxLines is greater than 0 (e.g., 2 in the issue example) and the current line
count reaches MaxLines, the last line is collapsed using textLine.Collapse(). However,
GetCollapsingProperties was being passed the maximum width of the entire TextLayout
instead of the width of that specific line, so ellipses were not being added correctly.

This issue has been fixed.

What is the current behavior?

When TextWrapping and MaxLines are both set, the last line that exceeds the maximum
line count is truncated without an ellipsis. For example:

<TextBlock Text="Very long text that wraps across multiple lines"
           TextWrapping="Wrap"
           MaxLines="2"
           TextTrimming="CharacterEllipsis" />

Result: The second line is truncated without "...".

What is the updated/expected behavior with this PR?

The ellipsis is consistently displayed on the last line when text exceeds the maximum
line count, matching the behavior of single-line TextBlock with TextTrimming set.

How was the solution implemented (if it's not obvious)?

Checklist

Breaking changes

None

Obsoletions / Deprecations

None

Fixed issues

Fixes #17633

puk06 added 2 commits August 19, 2026 17:22
When TextWrapping and MaxLines are set, the collapsing width was using the
layout's maximum width instead of the current line's width. This caused the
ellipsis to not appear when the last line was shorter than other lines.

Also added HasCollapsed check to prevent double-collapsing when text has
already been collapsed due to overflow.
@puk06

puk06 commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Screenshot

Before
Before

After
After

@MrJul MrJul added backport-candidate-12.1.x Consider this PR for backporting to 12.1 branch bug area-textprocessing labels Aug 19, 2026
MrJul
MrJul previously requested changes Aug 19, 2026

@MrJul MrJul left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs a matching unit test.

@avaloniaui-bot

Copy link
Copy Markdown

You can test this PR using the following package version. 12.2.999-cibuild0068485-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@puk06

puk06 commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

This needs a matching unit test.

Added! (Commit: afe15b2)

@puk06
puk06 requested a review from MrJul August 19, 2026 10:40
@avaloniaui-bot

Copy link
Copy Markdown

You can test this PR using the following package version. 12.2.999-cibuild0068495-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@puk06

puk06 commented Aug 20, 2026

Copy link
Copy Markdown
Contributor Author

If you think my test is a little redundant, I could modify it accordingly—what do you think?

Either way, I can verify whether the fix worked (since the results will definitely change when I run the test with the pre-fix implementation versus the post-fix implementation).

public void Should_Add_Ellipsis_When_MaxLines_Cuts_Short_Wrapped_Line(TextTrimming trimming)
{
    using (Start())
    {
        const string text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.";
        const int maxLines = 2;

        var layout = new TextLayout(
            text,
            Typeface.Default,
            12,
            Brushes.Black,
            textWrapping: TextWrapping.Wrap,
            textTrimming: trimming,
            maxWidth: 180,
            maxLines: maxLines);

        Assert.Equal(maxLines, layout.TextLines.Count);

        var lastLine = layout.TextLines[layout.TextLines.Count - 1];
        var formattedLineText = string.Concat(lastLine.TextRuns.Select(r => r.Text.ToString()));

        Assert.True(lastLine.HasCollapsed, "The wrapped line cut off by MaxLines must be collapsed.");
        Assert.Equal('\u2026', formattedLineText.Last());

        layout.Dispose();
    }
}

One drawback of this change is that the code becomes slightly harder to understand because it is written on the assumption that the length of the last line is always shorter than the layout’s maximum WidthIncludingTrailingWhitespace. However, since the last line should be shorter than the maximum line length in most cases, I don’t think this will be much of a problem.

@Gillibald Gillibald self-assigned this Aug 24, 2026
@Gillibald

Copy link
Copy Markdown
Contributor

I verified the change against upstream/main with real font metrics across a set of scenarios (wrap + MaxLines, hard breaks, RTL, CJK, overflowing single words, MaxLines=1). I could not find any regression.

Please simplify the test you have added. The test builds a text layout, scans for a line index that satisfies the bug condition, and then asserts cutIndex >= 0 with "Test data does not satisfy the bug-reproduction condition".

A test that can fail because its own input stopped qualifying is hard to maintain. The variant you posted in the comment is the one to take: fixed text, fixed maxWidth, MaxLines = 2, assert the line count, HasCollapsed, and that the last line text ends with the ellipsis.

MaxHeight has the same defect. It collapses against MaxWidth, so a wrapped line that is narrower than MaxWidth gets no ellipsis either. That is the same user-visible bug expressed through MaxHeight/LineHeight, which is how the scenario behaves under WPF. We do not need to cover it as part of this PR; we can handle it separately.

We should think about adding the ellipsis to the last line instead of collapsing if there is enough space available. Collapsing against the line's own width always buys the ellipsis by dropping content that would still have fit inside MaxWidth. On a line ending in a space, that costs the space, but a CJK line loses a character (...テキストで becomes ...テキスト... while 91 of 100 px were available) and WordEllipsis loses the whole last word. In my opinion, the real fix is to let the collapsing layer append the symbol without trimming when content is hidden for a reason other than width, which is what the issue was about.

In the end, this is about defining the desired behavior. This change ensures we trim, so it is a first step for the final solution.

@puk06

puk06 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

I verified the change against upstream/main with real font metrics across a set of scenarios (wrap + MaxLines, hard breaks, RTL, CJK, overflowing single words, MaxLines=1). I could not find any regression.

Please simplify the test you have added. The test builds a text layout, scans for a line index that satisfies the bug condition, and then asserts cutIndex >= 0 with "Test data does not satisfy the bug-reproduction condition".

A test that can fail because its own input stopped qualifying is hard to maintain. The variant you posted in the comment is the one to take: fixed text, fixed maxWidth, MaxLines = 2, assert the line count, HasCollapsed, and that the last line text ends with the ellipsis.

MaxHeight has the same defect. It collapses against MaxWidth, so a wrapped line that is narrower than MaxWidth gets no ellipsis either. That is the same user-visible bug expressed through MaxHeight/LineHeight, which is how the scenario behaves under WPF. We do not need to cover it as part of this PR; we can handle it separately.

We should think about adding the ellipsis to the last line instead of collapsing if there is enough space available. Collapsing against the line's own width always buys the ellipsis by dropping content that would still have fit inside MaxWidth. On a line ending in a space, that costs the space, but a CJK line loses a character (...テキストで becomes ...テキスト... while 91 of 100 px were available) and WordEllipsis loses the whole last word. In my opinion, the real fix is to let the collapsing layer append the symbol without trimming when content is hidden for a reason other than width, which is what the issue was about.

In the end, this is about defining the desired behavior. This change ensures we trim, so it is a first step for the final solution.

Thank you for the detailed review and the clear guidance on the test structure.

I have successfully refactored the test to fully adopt the specific variant you suggested (fixed text, fixed maxWidth, MaxLines = 2). This stabilizes the test setup as requested.

Regarding the other advanced architectural points discussed (such as MaxHeight logic and core ellipsis handling), I understand these are being addressed by separate PRs or are planned for later. Therefore, I will strictly limit the scope of this current PR to the fix for Wrapped text cut by MaxLines.

I appreciate your time and thorough testing!

@avaloniaui-bot

Copy link
Copy Markdown

You can test this PR using the following package version. 12.2.999-cibuild0068647-alpha. (feed url: https://nuget-feed-all.avaloniaui.net/v3/index.json) [PRBUILDID]

@MrJul
MrJul dismissed their stale review August 24, 2026 11:04

Changes made.

@MrJul
MrJul enabled auto-merge August 24, 2026 11:05
@MrJul
MrJul added this pull request to the merge queue Aug 24, 2026
Merged via the queue into AvaloniaUI:main with commit af9d9a7 Aug 24, 2026
9 checks passed
MrJul pushed a commit to MrJul/Avalonia that referenced this pull request Sep 2, 2026
…aloniaUI#22017)

* Fix TextBlock ellipsis not showing with TextWrapping and MaxLines

When TextWrapping and MaxLines are set, the collapsing width was using the
layout's maximum width instead of the current line's width. This caused the
ellipsis to not appear when the last line was shorter than other lines.

Also added HasCollapsed check to prevent double-collapsing when text has
already been collapsed due to overflow.

* Remove HasCollapsed check

* Add unit test for MaxLines trailing ellipsis on short wrapped lines

* Remove redundant comment

* fix: simplify TextLayout test
@MrJul MrJul removed the backport-candidate-12.1.x Consider this PR for backporting to 12.1 branch label Sep 2, 2026
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.

TextBlock TextTrimming=CharacterEllipsis, TextWrapping=Wrap, MaxLines="2" does not always show ellipsis

4 participants