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

[iOS] RecalculateSpanPositions causing crash #22469

Open
artemvalieiev opened this issue May 16, 2024 · 1 comment · May be fixed by #22487
Open

[iOS] RecalculateSpanPositions causing crash #22469

artemvalieiev opened this issue May 16, 2024 · 1 comment · May be fixed by #22487
Assignees
Labels
area-controls-label Label, Span i/regression This issue described a confirmed regression on a currently supported version platform/iOS 🍎 t/bug Something isn't working
Milestone

Comments

@artemvalieiev
Copy link
Contributor

Description

Using formatted text with custom font and mixed spans( see Steps to reproduce example of FormattedText that triggers exception) is causing crash.
I am happy to create a PR for this fix with my calculation (see workaround), but I need 1-2 days to get know UI tests.
8.0.3 was working because there wasn't this method. 8.0.21/ 8.0.40 contains this issue

Steps to Reproduce

  1. Create formatted string
public FormattedString CreateFormattedString()
{
    string upperText =
        "A culture of continuous learning isn't something you can ignore. 76% of employees are more inclined to stay when their workplace offers learning and development 🤔 Here's a few ways you can foster this:🧠 Create channels to encourage knowledge sharing between team members👏 Provide learning opportunities like job shadowing and rotation for hands-on experiences💬 Integrate routine feedback as a celebrated part of the workflow💻 Encourage your teams to dive into digital learning and live events Take a look at some more methods you can adopt from this recent article in Forbes 👇 🔗 ";

    var tapLinkGestureRecognizer = new TapGestureRecognizer
    {
        CommandParameter =
            "https://www.forbes.com/sites/forbeshumanresourcescouncil/2024/04/11/3-actionable-steps-for-managers-to-cultivate-a-learning-culture/",
        Command = new Command<string>(async (url) =>
        {
            Debug.WriteLine(url);
            await Launcher.OpenAsync(new Uri(url));
        })
    };

    return new FormattedString
    {
        Spans =
        {
            new Span { Text = upperText, FontFamily = "GilroyLight", FontSize = 14 },
            new Span
            {
                GestureRecognizers = { tapLinkGestureRecognizer },
                Text =
                    "https://www.forbes.com/sites/forbeshumanresourcescouncil/2024/04/11/3-actionable-steps-for-managers-to-cultivate-a-learning-culture/",
                FontFamily = "GilroyLight", FontSize = 14, TextColor = Colors.LightSkyBlue,
                TextDecorations = TextDecorations.Underline
            }
        }
    };
}
  1. On empty page add Label with margin 16
<?xml version="1.0" encoding="utf-8"?>

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MauiApp1.MainPage">

    <Grid>
        <Label x:Name="CrashLabel" FontFamily="GilroyLight" FontSize="14" FormattedText="{Binding FormattedString}" Margin="16,0" />
    </Grid>

</ContentPage>
  1. Assign FormattedText to CreateFormattedString() method result
protected override void OnAppearing()
{
    base.OnAppearing();
    this.CrashLabel.FormattedText = CreateFormattedString();
}

Link to public reproduction project repository

https://github.com/artemvalieiev/maui-ios-span-calc-crash

Version with bug

8.0.40 SR5

Is this a regression from previous behavior?

No, this is something new

Last version that worked well

8.0.3 GA

Affected platforms

iOS

Affected platform versions

No response

Did you find any workaround?

Yes, we use custom span region calculation, and we had to use custom Label because RecalculateSpanPositions is located in ArrangeOverride.

public partial class EngageMauiLabel_iOS
{
    protected override Size ArrangeOverride(Rect bounds)
    {
        Frame = this.ComputeFrame(bounds);
        Handler?.PlatformArrange(Frame);
        if (Handler?.PlatformView is UILabel label)
                label.RecalculateSpanPositions(this);
        return Frame.Size;
    }
}

public static class EngageMauiLabelExtensions
{
    public static void RecalculateSpanPositions(this UILabel control, Label element)
    {
        if (element is null)
            return;

        if (element.TextType == TextType.Html)
            return;

        if (element?.FormattedText?.Spans is null
            || element.FormattedText.Spans.Count == 0)
            return;

        var finalSize = control.Bounds;

        if (finalSize.Width <= 0 || finalSize.Height <= 0)
            return;

        var inline = control.AttributedText;

        if (inline is null)
            return;

        UpdateSpanRegions(control, element.FormattedText.Spans.ToList());
    }


    public static void UpdateSpanRegions(UILabel label, List<Span> spans)
    {
        var attributedText = label.AttributedText;
        if ((attributedText?.Length ?? 0) == 0)
            return;

        using var textStorage = new NSTextStorage();
        using var layoutManager = new NSLayoutManager();
        using var textContainer = new NSTextContainer();

        textStorage.AddLayoutManager(layoutManager);
        layoutManager.AddTextContainer(textContainer);

        textContainer.LineFragmentPadding = 0;
        textContainer.Size = new(label.Bounds.Width, nfloat.MaxValue);

        textStorage.SetString(attributedText!);

        layoutManager.EnsureLayoutForTextContainer(textContainer);

        int currentIndex = 0;
        
        foreach (var span in spans)
        {
            if (string.IsNullOrEmpty(span.Text))
                continue;

            var spanRects = new List<CGRect>();
            var spanStartIndex = currentIndex;
            var spanEndIndex = currentIndex + span.Text.Length - 1;

            var startGlyphRange = layoutManager.GetGlyphRange(new NSRange(spanStartIndex, 1));
            var endGlyphRange = layoutManager.GetGlyphRange(new NSRange(spanEndIndex, 1));

            void EnumerateLineFragmentCallback(CGRect rect, CGRect usedRect, NSTextContainer textContainer,
                NSRange lineGlyphRange, out bool stop)
            {
                
                //Whole span is within the line and bigger than the line
                if(lineGlyphRange.Location >= startGlyphRange.Location &&
                   NSMaxRange(lineGlyphRange) <= endGlyphRange.Location)
                {
                    spanRects.Add(usedRect);
                }
                // Whole span is within the line and smaller than the line
                else if (lineGlyphRange.Location <= startGlyphRange.Location &&
                         endGlyphRange.Location <= NSMaxRange(lineGlyphRange))
                {
                    var spanBoundingRect = layoutManager!.GetBoundingRect(
                        new(startGlyphRange.Location, endGlyphRange.Location - startGlyphRange.Location + 1),
                        textContainer);
                    spanRects.Add(spanBoundingRect);
                }
                // Span starts on current line and ends on next lines
                else if (lineGlyphRange.Location <= startGlyphRange.Location &&
                         NSMaxRange(lineGlyphRange) <= endGlyphRange.Location)
                {
                    var spanBoundingRect = layoutManager!.GetBoundingRect(
                        new(startGlyphRange.Location, NSMaxRange(lineGlyphRange) - startGlyphRange.Location + 1),
                        textContainer);
                    spanRects.Add(spanBoundingRect);
                }
                // Span starts on previous lines and ends on current line
                else if (lineGlyphRange.Location >= startGlyphRange.Location &&
                         NSMaxRange(lineGlyphRange) >= endGlyphRange.Location)
                {
                    var spanBoundingRect = layoutManager!.GetBoundingRect(
                        new(lineGlyphRange.Location, endGlyphRange.Location - lineGlyphRange.Location + 1),
                        textContainer);
                    spanRects.Add(spanBoundingRect);
                }

                stop = false;
            }

            layoutManager.EnumerateLineFragments(
                new(startGlyphRange.Location, endGlyphRange.Location - startGlyphRange.Location + 1),
                EnumerateLineFragmentCallback);

            var inflate = span.GestureRecognizers.Count > 0 ? 4 : -4;
            var region = Region.FromRectangles(spanRects.Select(r => new Rect(r.X, r.Y, r.Width, r.Height)))
                .Inflate(inflate);
            (span as ISpatialElement).Region = region;
            
            currentIndex += span.Text.Length;
        }
    }

    public static nint NSMaxRange(NSRange range) => range.Location + range.Length;
}
```,

### Relevant log output

```shell
System.ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection. (Parameter 'index')
   at System.Collections.Generic.List`1[[System.Double, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].get_Item(Int32 index)
   at Microsoft.Maui.Controls.Platform.FormattedStringExtensions.CreateSpanRects(CGRect startRect, CGRect endRect, List`1 lineHeights, List`1 multilineRects, Boolean endsWithNewLine)
   at Microsoft.Maui.Controls.Platform.FormattedStringExtensions.GetMultilinedBounds(NSRange characterRange, NSLayoutManager layoutManager, NSTextContainer textContainer, CGRect startRect, CGRect endRect, List`1 lineHeights, Boolean endsWithNewLine)
   at Microsoft.Maui.Controls.Platform.FormattedStringExtensions.RecalculateSpanPositions(UILabel control, Label element)
   at Microsoft.Maui.Controls.Label.RecalculateSpanPositions()
   at Microsoft.Maui.Controls.Label.ArrangeOverride(Rect bounds)
   at Microsoft.Maui.Controls.VisualElement.Microsoft.Maui.IView.Arrange(Rect bounds)
   at Microsoft.Maui.Layouts.GridLayoutManager.ArrangeChildren(Rect bounds)
   at Microsoft.Maui.Controls.Layout.CrossPlatformArrange(Rect bounds)
   at Microsoft.Maui.Platform.MauiView.CrossPlatformArrange(Rect bounds)
   at Microsoft.Maui.Platform.MauiView.LayoutSubviews()
   at UIKit.UIApplication.UIApplicationMain(Int32 argc, String[] argv, IntPtr principalClassName, IntPtr delegateClassName) in /Users/builder/azdo/_work/1/s/xamarin-macios/src/UIKit/UIApplication.cs:line 58
@artemvalieiev artemvalieiev added the t/bug Something isn't working label May 16, 2024
Copy link
Contributor

Hi I'm an AI powered bot that finds similar issues based off the issue title.

Please view the issues below to see if they solve your problem, and if the issue describes your problem please consider closing this one and thumbs upping the other issue to help us prioritize it. Thank you!

Open similar issues:

Closed similar issues:

Note: You can give me feedback by thumbs upping or thumbs downing this comment.

@PureWeen PureWeen added the area-controls-label Label, Span label May 16, 2024
@artemvalieiev artemvalieiev linked a pull request May 17, 2024 that will close this issue
@PureWeen PureWeen added this to the .NET 8 SR6 milestone May 17, 2024
@PureWeen PureWeen added platform/iOS 🍎 i/regression This issue described a confirmed regression on a currently supported version labels May 17, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-controls-label Label, Span i/regression This issue described a confirmed regression on a currently supported version platform/iOS 🍎 t/bug Something isn't working
Projects
Status: Todo
Development

Successfully merging a pull request may close this issue.

3 participants