Skip to content

Commit

Permalink
Fix Label Multiline Truncation
Browse files Browse the repository at this point in the history
Co-authored-by: Javier Suárez <javiersuarezruiz@hotmail.com>
  • Loading branch information
mattleibow and jsuarezruiz committed Jan 18, 2024
1 parent 3ed0bb7 commit d87f3aa
Show file tree
Hide file tree
Showing 10 changed files with 179 additions and 59 deletions.
Expand Up @@ -231,6 +231,13 @@
<Label
LineBreakMode ="TailTruncation"
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." />
<Label
Text="TailTruncation (with 2 MaxLines)"
Style="{StaticResource Headline}" />
<Label
MaxLines="2"
LineBreakMode ="TailTruncation"
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." />
<Label
Text="CharacterWrap"
Style="{StaticResource Headline}" />
Expand Down
1 change: 0 additions & 1 deletion src/Controls/src/Core/Label/Label.Android.cs
Expand Up @@ -36,7 +36,6 @@ private protected override void OnHandlerChangedCore()
public static void MapText(LabelHandler handler, Label label) => MapText((ILabelHandler)handler, label);
public static void MapLineBreakMode(LabelHandler handler, Label label) => MapLineBreakMode((ILabelHandler)handler, label);


public static void MapText(ILabelHandler handler, Label label)
{
Platform.TextViewExtensions.UpdateText(handler.PlatformView, label);
Expand Down
5 changes: 2 additions & 3 deletions src/Controls/src/Core/Label/Label.Windows.cs
Expand Up @@ -8,17 +8,16 @@ public partial class Label
public static void MapDetectReadingOrderFromContent(LabelHandler handler, Label label) => MapDetectReadingOrderFromContent((ILabelHandler)handler, label);
public static void MapText(LabelHandler handler, Label label) => MapText((ILabelHandler)handler, label);


public static void MapDetectReadingOrderFromContent(ILabelHandler handler, Label label) =>
Platform.TextBlockExtensions.UpdateDetectReadingOrderFromContent(handler.PlatformView, label);

public static void MapText(ILabelHandler handler, Label label) =>
Platform.TextBlockExtensions.UpdateText(handler.PlatformView, label);

public static void MapLineBreakMode(ILabelHandler handler, Label label) =>
handler.PlatformView?.UpdateLineBreakMode(label.LineBreakMode);
handler.PlatformView?.UpdateLineBreakMode(label);

public static void MapMaxLines(ILabelHandler handler, Label label) =>
handler.PlatformView?.UpdateMaxLines(label);
}
}
}
2 changes: 1 addition & 1 deletion src/Controls/src/Core/Label/Label.iOS.cs
@@ -1,4 +1,4 @@
#nullable disable
#nullable disable
using System;
using Microsoft.Maui.Controls.Platform;

Expand Down
Expand Up @@ -2,6 +2,7 @@
using System;
using Android.Text;
using Android.Widget;
using AndroidX.AppCompat.Widget;
using Microsoft.Maui.Controls.Internals;

namespace Microsoft.Maui.Controls.Platform
Expand Down Expand Up @@ -31,14 +32,21 @@ public static void UpdateLineBreakMode(this TextView textView, Label label)

public static void UpdateMaxLines(this TextView textView, Label label)
{
// Linebreak mode also handles settng MaxLines
// Linebreak mode also handles setting MaxLines
textView.SetLineBreakMode(label.LineBreakMode, label.MaxLines);
}

internal static void SetLineBreakMode(this TextView textView, LineBreakMode lineBreakMode, int? maxLines = null)
{
if (!maxLines.HasValue || maxLines <= 0)
maxLines = int.MaxValue;
{
// Without setting the MaxLines property, to equalize behaviors across platforms
// we set the max to 1.
if (lineBreakMode == LineBreakMode.TailTruncation)
maxLines = 1;
else
maxLines = int.MaxValue;
}

bool singleLine = false;
bool shouldSetSingleLine = !OperatingSystem.IsAndroidVersionAtLeast(23);
Expand Down Expand Up @@ -67,12 +75,11 @@ internal static void SetLineBreakMode(this TextView textView, LineBreakMode line
break;
case LineBreakMode.TailTruncation:

// Leaving this in for now to preserve existing behavior
// Technically, we don't _need_ this for Labels; they will handle Ellipsization at the end just fine, even with multiple lines
// But we don't have a mechanism for setting MaxLines on other controls (e.g., Button) right now, so we need to force it here or
// they will potentially exceed a single line. Also, changing this behavior the for Labels would technically be breaking (though
// possibly less surprising than what happens currently).
maxLines = 1;
// We don't have a mechanism for setting MaxLines on other controls (e.g., Button) right now, so we need to force it here or
// they will potentially exceed a single line.
if (textView is AppCompatButton)
maxLines = 1;

textView.Ellipsize = TextUtils.TruncateAt.End;
break;
case LineBreakMode.MiddleTruncation:
Expand Down
Expand Up @@ -12,44 +12,11 @@ namespace Microsoft.Maui.Controls.Platform
internal static class TextBlockExtensions
{
public static void UpdateLineBreakMode(this TextBlock textBlock, Label label) =>
textBlock.UpdateLineBreakMode(label.LineBreakMode);
textBlock.SetLineBreakMode(label.LineBreakMode, label.MaxLines);

public static void UpdateLineBreakMode(this TextBlock textBlock, LineBreakMode lineBreakMode)
{
if (textBlock == null)
return;

switch (lineBreakMode)
{
case LineBreakMode.NoWrap:
textBlock.TextTrimming = TextTrimming.Clip;
textBlock.TextWrapping = TextWrapping.NoWrap;
break;
case LineBreakMode.WordWrap:
textBlock.TextTrimming = TextTrimming.None;
textBlock.TextWrapping = TextWrapping.Wrap;
break;
case LineBreakMode.CharacterWrap:
textBlock.TextTrimming = TextTrimming.WordEllipsis;
textBlock.TextWrapping = TextWrapping.Wrap;
break;
case LineBreakMode.HeadTruncation:
// TODO: This truncates at the end.
textBlock.TextTrimming = TextTrimming.WordEllipsis;
DetermineTruncatedTextWrapping(textBlock);
break;
case LineBreakMode.TailTruncation:
textBlock.TextTrimming = TextTrimming.CharacterEllipsis;
DetermineTruncatedTextWrapping(textBlock);
break;
case LineBreakMode.MiddleTruncation:
// TODO: This truncates at the end.
textBlock.TextTrimming = TextTrimming.WordEllipsis;
DetermineTruncatedTextWrapping(textBlock);
break;
default:
throw new ArgumentOutOfRangeException();
}
textBlock.SetLineBreakMode(lineBreakMode, null);
}

static void DetermineTruncatedTextWrapping(TextBlock textBlock) =>
Expand Down Expand Up @@ -87,10 +54,8 @@ public static double FindDefaultLineHeight(this TextBlock control, Inline inline

public static void UpdateMaxLines(this TextBlock platformControl, Label label)
{
if (label.MaxLines >= 0)
platformControl.MaxLines = label.MaxLines;
else
platformControl.MaxLines = 0;
// Linebreak mode also handles setting MaxLines
platformControl.SetLineBreakMode(label.LineBreakMode, label.MaxLines);
}

public static void UpdateDetectReadingOrderFromContent(this TextBlock platformControl, Label label)
Expand All @@ -99,6 +64,46 @@ public static void UpdateDetectReadingOrderFromContent(this TextBlock platformCo
platformControl.SetTextReadingOrder(label.OnThisPlatform().GetDetectReadingOrderFromContent());
}

internal static void SetLineBreakMode(this TextBlock textBlock, LineBreakMode lineBreakMode, int? maxLines = null)
{
if (maxLines.HasValue && maxLines >= 0)
textBlock.MaxLines = maxLines.Value;
else
textBlock.MaxLines = 0;

switch (lineBreakMode)
{
case LineBreakMode.NoWrap:
textBlock.TextTrimming = TextTrimming.Clip;
textBlock.TextWrapping = TextWrapping.NoWrap;
break;
case LineBreakMode.WordWrap:
textBlock.TextTrimming = TextTrimming.None;
textBlock.TextWrapping = TextWrapping.Wrap;
break;
case LineBreakMode.CharacterWrap:
textBlock.TextTrimming = TextTrimming.WordEllipsis;
textBlock.TextWrapping = TextWrapping.Wrap;
break;
case LineBreakMode.HeadTruncation:
// TODO: This truncates at the end.
textBlock.TextTrimming = TextTrimming.WordEllipsis;
DetermineTruncatedTextWrapping(textBlock);
break;
case LineBreakMode.TailTruncation:
textBlock.TextTrimming = TextTrimming.CharacterEllipsis;
DetermineTruncatedTextWrapping(textBlock);
break;
case LineBreakMode.MiddleTruncation:
// TODO: This truncates at the end.
textBlock.TextTrimming = TextTrimming.WordEllipsis;
DetermineTruncatedTextWrapping(textBlock);
break;
default:
throw new ArgumentOutOfRangeException();
}
}

internal static void SetTextReadingOrder(this TextBlock platformControl, bool detectReadingOrderFromContent) =>
platformControl.TextReadingOrder = detectReadingOrderFromContent
? TextReadingOrder.DetectFromContent
Expand Down
Expand Up @@ -72,8 +72,14 @@ public static void UpdateMaxLines(this UILabel platformLabel, Label label)
internal static void SetLineBreakMode(this UILabel platformLabel, Label label)
{
int maxLines = label.MaxLines;

if (maxLines < 0)
maxLines = 0;
{
if (label.LineBreakMode == LineBreakMode.TailTruncation)
maxLines = 1;
else
maxLines = 0;
}

switch (label.LineBreakMode)
{
Expand All @@ -97,7 +103,6 @@ internal static void SetLineBreakMode(this UILabel platformLabel, Label label)
break;
case LineBreakMode.TailTruncation:
platformLabel.LineBreakMode = UILineBreakMode.TailTruncation;
maxLines = 1;
break;
}

Expand Down
Expand Up @@ -66,6 +66,28 @@ public async Task VerticalTextAlignedWhenRtlIsFalse()
Assert.True(platformLabel.Gravity.HasFlag(GravityFlags.CenterVertical), "Label should only have the CenterVertical flag.");
}

// https://github.com/dotnet/maui/issues/18059
[Fact(DisplayName = "Using TailTruncation LineBreakMode with 2 MaxLines")]
public async Task UsingTailTruncationWith2MaxLines()
{
var label = new Label()
{
Text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit",
LineBreakMode = LineBreakMode.TailTruncation,
MaxLines = 2
};

var handler = await CreateHandlerAsync<LabelHandler>(label);

var platformLabel = GetPlatformLabel(handler);

await InvokeOnMainThreadAsync((System.Action)(() =>
{
Assert.Equal(2, GetPlatformMaxLines(handler));
Assert.Equal(LineBreakMode.TailTruncation.ToPlatform(), GetPlatformLineBreakMode(handler));
}));
}

TextView GetPlatformLabel(LabelHandler labelHandler) =>
labelHandler.PlatformView;

Expand Down
60 changes: 59 additions & 1 deletion src/Controls/tests/DeviceTests/Elements/Label/LabelTests.cs
@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;
#if __IOS__
using Foundation;
#endif
Expand Down Expand Up @@ -26,6 +27,37 @@ void SetupBuilder()
});
});
}

[Fact(DisplayName = "Does Not Leak")]
public async Task DoesNotLeak()
{
SetupBuilder();

WeakReference viewReference = null;
WeakReference platformViewReference = null;
WeakReference handlerReference = null;

await InvokeOnMainThreadAsync(() =>
{
var layout = new Grid();
var label = new Label
{
Text = "Test"
};
layout.Add(label);
var handler = CreateHandler<LayoutHandler>(layout);
viewReference = new WeakReference(label);
handlerReference = new WeakReference(label.Handler);
platformViewReference = new WeakReference(label.Handler.PlatformView);
});

await AssertionExtensions.WaitForGC(viewReference, handlerReference, platformViewReference);
Assert.False(viewReference.IsAlive, "Label should not be alive!");
Assert.False(handlerReference.IsAlive, "Handler should not be alive!");
Assert.False(platformViewReference.IsAlive, "PlatformView should not be alive!");
}

[Theory]
[ClassData(typeof(TextTransformCases))]
Expand Down Expand Up @@ -204,6 +236,32 @@ public async Task LineBreakModeDoesNotAffectMaxLines()
}));
}

[Fact(DisplayName = "LineBreakMode TailTruncation does not affect MaxLines")]
public async Task TailTruncationDoesNotAffectMaxLines()
{
var label = new Label()
{
Text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit",
MaxLines = 3,
LineBreakMode = LineBreakMode.TailTruncation,
};

var handler = await CreateHandlerAsync<LabelHandler>(label);
var platformLabel = GetPlatformLabel(handler);

await InvokeOnMainThreadAsync(() =>
{
Assert.Equal(3, GetPlatformMaxLines(handler));
Assert.Equal(LineBreakMode.TailTruncation.ToPlatform(), GetPlatformLineBreakMode(handler));
label.LineBreakMode = LineBreakMode.CharacterWrap;
platformLabel.UpdateLineBreakMode(label);
Assert.Equal(3, GetPlatformMaxLines(handler));
Assert.Equal(LineBreakMode.CharacterWrap.ToPlatform(), GetPlatformLineBreakMode(handler));
});
}

[Fact(DisplayName = "MaxLines Initializes Correctly")]
public async Task MaxLinesInitializesCorrectly()
{
Expand Down
26 changes: 22 additions & 4 deletions src/Controls/tests/DeviceTests/Elements/Label/LabelTests.iOS.cs
@@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Handlers;
Expand All @@ -12,6 +9,27 @@ namespace Microsoft.Maui.DeviceTests
{
public partial class LabelTests
{

[Fact(DisplayName = "Using TailTruncation LineBreakMode changes MaxLines")]
public async Task UsingTailTruncationSetMaxLines()
{
var label = new Label()
{
Text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit",
LineBreakMode = LineBreakMode.TailTruncation,
};

var handler = await CreateHandlerAsync<LabelHandler>(label);

var platformLabel = GetPlatformLabel(handler);

await InvokeOnMainThreadAsync((System.Action)(() =>
{
Assert.Equal(1, GetPlatformMaxLines(handler));
Assert.Equal(LineBreakMode.TailTruncation.ToPlatform(), GetPlatformLineBreakMode(handler));
}));
}

UILabel GetPlatformLabel(LabelHandler labelHandler) =>
(UILabel)labelHandler.PlatformView;

Expand Down

0 comments on commit d87f3aa

Please sign in to comment.