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

Gestures don't work on Label Spans #4734

Closed
4 tasks done
Tracked by #8895
davidortinau opened this issue Feb 17, 2022 · 51 comments · Fixed by #14410, #15544 or #17731
Closed
4 tasks done
Tracked by #8895

Gestures don't work on Label Spans #4734

davidortinau opened this issue Feb 17, 2022 · 51 comments · Fixed by #14410, #15544 or #17731
Assignees
Labels
area/controls 🎮 Label, Button, CheckBox, Slider, Stepper, Switch, Picker, Entry, Editor area/gestures control-label Label, Span delighter fixed-in-8.0.0-preview.5.8529 Look for this fix in 8.0.0-preview.5.8529! fixed-in-8.0.10 fixed-in-8.0.14 fixed-in-9.0.0-preview.2.10247 fixed-in-9.0.0-preview.2.10293 p/1 Work that is critical for the release, but we could probably ship without partner/cat 😻 Client CAT Team platform/android 🤖 platform/windows 🪟 t/bug Something isn't working
Projects
Milestone

Comments

@davidortinau
Copy link
Contributor

davidortinau commented Feb 17, 2022

Description

I added a gesture to a label span and tapped it. Nothing is happening on Windows. Cannot test Android bc it doesn't render the span.

Platforms:

Steps to Reproduce

Run this:

<?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="ControlGallery.Pages.LabelPage"
             Shell.FlyoutBehavior="Disabled"
             >

    <ContentPage.Content>
        <ScrollView>
            <StackLayout Padding="{OnPlatform iOS='30,60,30,30', Default='30'}">

                <Label LineBreakMode="NoWrap" LineHeight="1.4">
                    <Label.FormattedText>
                        <FormattedString>
                            <Span Text="Font name: Default &#10;"/>
                            <Span Text="Version: 1.00  &#10;"/>
                            <Span Text="Digitally Signed, TrueType Outlines &#10;"/>
                            <Span Text="abcdefghijklmnopqrstuvwxyz "/>
                            <Span Text="abcdefghijklmnopqrstuvwxyz &#10;" TextTransform="Uppercase"/>
                            <Span Text="1234567890.:,;'+-*/=  &#10;"/>
                            <Span Text="12 The quick brown fox jumps over the lazy dog. 1234567890 &#10;" FontSize="12"/>
                            <Span Text="18 The quick brown fox jumps over the lazy dog. 1234567890 &#10;" FontSize="18"/>
                            <Span Text="24 The quick brown fox jumps over the lazy dog. 1234567890 &#10;" FontSize="24"/>
                            <Span Text="36 The quick brown fox jumps over the lazy dog. 1234567890 &#10;" FontSize="36"/>
                            <Span Text="48 The quick brown fox jumps over the lazy dog. 1234567890 &#10;" FontSize="48"/>
                            <Span Text="60 The quick brown fox jumps over the lazy dog. 1234567890 &#10;" FontSize="60"/>
                            <Span Text="72 The quick brown fox jumps over the lazy dog. 1234567890 " FontSize="72" >
                                <Span.GestureRecognizers>
                                    <TapGestureRecognizer Command="{Binding TapCommand}" CommandParameter="https://google.com" />
                                </Span.GestureRecognizers>
                            </Span>
                        </FormattedString>
                    </Label.FormattedText>                    
                </Label>
            </StackLayout>
        </ScrollView>

    </ContentPage.Content>
</ContentPage>
using System.Windows.Input;

namespace ControlGallery.Pages;

public partial class LabelPage : ContentPage
{
    public ICommand TapCommand => new Command<string>(async (url) => await Launcher.OpenAsync(url));

    public LabelPage()
    {
        InitializeComponent();
        BindingContext = this;
    }
}

image

Screenshot_1645061133

Version with bug

Preview 13 (current)

Last version that worked well

Unknown/Other

Affected platforms

Android, Windows

Affected platform versions

All

Did you find any workaround?

No workaround

Relevant log output

No response

@davidortinau davidortinau added the t/bug Something isn't working label Feb 17, 2022
@jsuarezruiz jsuarezruiz added this to New in Triage via automation Feb 17, 2022
@jsuarezruiz jsuarezruiz added area/Xaml </> Controls - XAML, CSS, Gestures, Triggers, Behaviors fatal labels Feb 17, 2022
@jsuarezruiz jsuarezruiz moved this from New to Ready For Work in Triage Feb 17, 2022
@YuqinSong YuqinSong added s/verified Verified / Reproducible Issue ready for Engineering Triage and removed s/verified Verified / Reproducible Issue ready for Engineering Triage labels Feb 25, 2022
@XamlTest XamlTest added s/triaged Issue has been reviewed s/verified Verified / Reproducible Issue ready for Engineering Triage labels Feb 25, 2022
@XamlTest
Copy link
Collaborator

Reproduced on Windows(OS: 19044.1526) and Android(OS: Android 12.0-API31).
Repro Project: 4734-Gesture.zip

@PureWeen PureWeen added this to the 6.0.300-rc.2 milestone Mar 22, 2022
@Redth Redth modified the milestones: 6.0.300-rc.2, 6.0.300-rc.3 Apr 20, 2022
@davidortinau davidortinau added p/1 Work that is critical for the release, but we could probably ship without and removed fatal labels Apr 25, 2022
@Redth Redth modified the milestones: 6.0.300-rc.3, 6.0.300 Apr 27, 2022
@PureWeen PureWeen self-assigned this Apr 28, 2022
@davidortinau davidortinau changed the title Span Gesture doesn't work Gestures don't work on Label Spans Apr 29, 2022
@AndreduToit
Copy link

Confirmed - #6257 is a duplicate of this issue. Added to the 6.0.300 milestone and self-assigned by PureWeen - thanks.

@Redth Redth added p/2 Work that is important, but not critical for the release and removed p/1 Work that is critical for the release, but we could probably ship without labels May 3, 2022
@samhouts samhouts modified the milestones: 6.0.300, 6.0.300-servicing May 7, 2022
@PureWeen PureWeen removed their assignment Aug 3, 2022
@danielftz
Copy link

This issue persists. ETA on the fix?

@philipag
Copy link

@PureWeen, @samhouts Did this fall through the cracks? It's listed as being part of 6.0.300-servicing, but no fix made it into even the latest release.

@PureWeen PureWeen modified the milestones: 6.0-servicing, Backlog Aug 23, 2022
@komaramrvk
Copy link

I found work around for this issue.

                <Label
                    x:Name="consentLabel"
                    Grid.Row="2">
                    <Label.FormattedText>
                        <FormattedString>
                            <Span
                                Text="{Binding TextLink[0]}" />
                            <Span
                                FontAttributes="Bold"
                                Text="Privacy policy"
                                TextDecorations="Underline" >
                                <Span.GestureRecognizers>
                                    <TapGestureRecognizer Command="{Binding PrivacyPolicyTapCommand}" />
                                </Span.GestureRecognizers>
                            </Span>
                            <Span
                                Text="{Binding TextLink[2]}" />
                        </FormattedString>
                    </Label.FormattedText>
                    <Label.GestureRecognizers>
                        <TapGestureRecognizer Tapped="TapGestureRecognizer_Tapped"/>
                    </Label.GestureRecognizers>
                </Label>

adding TapGestureRecognizer to the label is fixing the issue. And nothing to add in code behind.
private void TapGestureRecognizer_Tapped(object sender, TappedEventArgs e)
{
//leave this method as empty. so span command will work
}

@riccardominato
Copy link

@komaramrvk I tried copying your code and it's still not working for me on iOS 17.2. What platform are you working on?

@cagriy
Copy link

cagriy commented Jan 7, 2024

This works on IOS and Android as intended

                    <HorizontalStackLayout>
                        <Label x:Name="LblTestLink">
                            <Label.FormattedText>
                                <FormattedString>
                                    <Span Text="Test Link"
                                          TextColor="Blue"
                                          TextDecorations="Underline">
                                    </Span>
                                </FormattedString>
                            </Label.FormattedText>
                            <Label.GestureRecognizers>
                                <TapGestureRecognizer Tapped="Links_OnTapped" />
                            </Label.GestureRecognizers>
                        </Label>
                        <Label Text=" and " />
                        ...
                        ...
                    </HorizontalStackLayout>

@Redth Redth assigned jsuarezruiz and emaf and unassigned mattleibow and jsuarezruiz Jan 10, 2024
@jameslavery
Copy link

Still not working on Windows. This is a real pain. I need to be able to allow users to click on a link in formatted text. Clicking 'anywhere' on the text, as in the workarounds above, is not much use.

Any news on when this is going to be fixed? Not holding my breath....

@RedZone908
Copy link

RedZone908 commented Jan 12, 2024

Still not working on iOS... using a label link instead WORKS sure but it can be pretty janky alignment-wise so span links are preferred, only they don't work in iOS. I hope someone is looking at this still!

@artemvalieiev
Copy link
Contributor

@RedZone908 @riccardominato @WhoeverReadsThisIsStupid @mobilewares @donatellijl12 to anyone who needs a fix right now on iOS, you can fix the label handler by adding this class and overwrite default Handler it as part of ConfigureMauiHandlers

#if IOS 
ConfigureMauiHandlers((handlers) => handlers.AddHandler<Label,LabelHandlerFix15544>());
#endif
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Handlers;
using UIKit;
using Foundation;
using CoreGraphics;

namespace YourNameSpace;

/// <summary>
/// Provides calculations for spatial regions of a <see cref="Label"/> that has a <see cref="Label.FormattedText"/> property set.
/// </summary>
public class LabelHandlerFix15544 : LabelHandler
{
    public LabelHandlerFix15544()
    {
    }

    public LabelHandlerFix15544(PropertyMapper mapper) : base(mapper)
    {
    }

    public LabelHandlerFix15544(PropertyMapper mapper, CommandMapper commandMapper) 
        : base(mapper, commandMapper)
    {
    }

    public override void PlatformArrange(Rect rect)
    {
        base.PlatformArrange(rect);
        RecalculateSpanPositions(this);
    }

    static void RecalculateSpanPositions(LabelHandler labelHandler)
    {
        if (labelHandler.PlatformView is not UILabel platformView ||
            labelHandler.VirtualView is not Label virtualView)
            return;

        RecalculateSpanPositions(platformView, virtualView);
    }

    public static void RecalculateSpanPositions(UILabel control, Label element)
    {
        if (element == null)
            return;

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

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

        var finalSize = control.Frame;

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

        var inline = control.AttributedText;

        if (inline is null)
            return;

        NSTextStorage textStorage = new NSTextStorage();
        textStorage.SetString(inline);

        var layoutManager = new NSLayoutManager();
        textStorage.AddLayoutManager(layoutManager);

        var textContainer = new NSTextContainer(size: finalSize.Size)
        {
            LineFragmentPadding = 0
        };

        layoutManager.AddTextContainer(textContainer);

        var currentLocation = 0;

        for (int i = 0; i < element.FormattedText.Spans.Count; i++)
        {
            var span = element.FormattedText.Spans[i];

            var location = currentLocation;
            var length = span.Text?.Length ?? 0;

            if (length == 0)
                continue;

            var startRect = GetCharacterBounds(new NSRange(location, 1), layoutManager, textContainer);
            var endRect = GetCharacterBounds(new NSRange(location + length, 1), layoutManager, textContainer);

            var defaultLineHeight = FindDefaultLineHeight(control, location, length);

            var yaxis = startRect.Top;
            var lineHeights = new List<double>();

            while ((endRect.Bottom - yaxis) > 0.001)
            {
                double lineHeight;
                if (yaxis == startRect.Top) // First Line
                {
                    lineHeight = startRect.Bottom - startRect.Top;
                }
                else if (yaxis != endRect.Top) // Middle Line(s)
                {
                    lineHeight = defaultLineHeight;
                }
                else // Bottom Line
                {
                    lineHeight = endRect.Bottom - endRect.Top;
                }

                lineHeights.Add(lineHeight);
                yaxis += (float) lineHeight;
            }

            ((ISpatialElement) span).Region = Region
                .FromLines(lineHeights.ToArray(), finalSize.Width, startRect.X, endRect.X, startRect.Top).Inflate(10);

            // Update current location
            currentLocation += length;
        }
    }

    static CGRect GetCharacterBounds(NSRange characterRange, NSLayoutManager layoutManager,
        NSTextContainer textContainer)
    {
        layoutManager.GetCharacterRange(characterRange, out NSRange glyphRange);

        return layoutManager.GetBoundingRect(glyphRange, textContainer);
    }

    static double FindDefaultLineHeight(UILabel control, int start, int length)
    {
        if (length == 0)
            return 0.0;

        var textStorage = new NSTextStorage();

        if (control.AttributedText is not null)
            textStorage.SetString(control.AttributedText.Substring(start, length));

        var layoutManager = new NSLayoutManager();
        textStorage.AddLayoutManager(layoutManager);

        var textContainer = new NSTextContainer(size: new SizeF(float.MaxValue, float.MaxValue))
        {
            LineFragmentPadding = 0
        };
        layoutManager.AddTextContainer(textContainer);

        var rect = GetCharacterBounds(new NSRange(0, 1), layoutManager, textContainer);
        return rect.Bottom - rect.Top;
    }
}

This adds code from #15544 and you can use TapGestureRecognizer on spans. As soon as PR will be fixed you can delete code above and remove the registration you added

@RedZone908
Copy link

@artemvalieiev this worked! Thank you so much! However, for other potential users - @riccardominato @WhoeverReadsThisIsStupid @mobilewares @donatellijl12 - be aware that you must enclose the entire LabelHandlerFix15544.cs file's contents in an #if IOS ... #endif because the namespaces UIKit, Foundation, and CoreGraphics only exist in that context. (at least, when I didn't enclose it like that, it couldn't build for Android)

@psatheesh18
Copy link

psatheesh18 commented Feb 6, 2024

I have found a workaround and it is working .net 7

Xaml: For android and iOS

             <Label>
                <Label.GestureRecognizers>
                    <TapGestureRecognizer/>
                </Label.GestureRecognizers>
                <Label.FormattedText>
                    <FormattedString>
                        <Span Text="First"/>
                        <Span Text="Click here">
                            <Span.GestureRecognizers>
                                <TapGestureRecognizer/>
                            </Span.GestureRecognizers>
                        </Span>
                        <Span Text="last"/>
                    </FormattedString>
                </Label.FormattedText>
            </Label>

ios handler

      public class LabelhandlerEx : LabelHandler
      {
         protected override void ConnectHandler(MauiLabel platformView)
        {
            base.ConnectHandler(platformView);

            if(VirtualView is Label label)
            {
                label.SizeChanged += Label_SizeChanged;
            }
        }

        private void Label_SizeChanged(object sender, EventArgs e)
        {
            PlatformView.RecalculateSpanPositions((Label)VirtualView);
        }
    }

    internal static class LabelExtensions
    {
        public static void RecalculateSpanPositions(this NativeLabel control, Label element)
        {
            if (element == null)
                return;

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

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

            var finalSize = control.Frame;

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

            var inline = control.AttributedText;

            var range = new NSRange(0, inline.Length);

            NSTextStorage textStorage = new NSTextStorage();
            textStorage.SetString(inline);

            var layoutManager = new NSLayoutManager();
            textStorage.AddLayoutManager(layoutManager);

            var textContainer = new NSTextContainer(size: finalSize.Size)
            {
                LineFragmentPadding = 0
            };

            layoutManager.AddTextContainer(textContainer);

            var labelWidth = finalSize.Width;

            var currentLocation = 0;

            for (int i = 0; i < element.FormattedText.Spans.Count; i++)
            {
                var span = element.FormattedText.Spans[i];

                var location = currentLocation;
                var length = span.Text?.Length ?? 0;

                if (length == 0)
                    continue;

                var startRect = GetCharacterBounds(new NSRange(location, 1), layoutManager, textContainer);
                var endRect = GetCharacterBounds(new NSRange(location + length, 1), layoutManager, textContainer);

                var startLineHeight = startRect.Bottom - startRect.Top;
                var endLineHeight = endRect.Bottom - endRect.Top;

                var defaultLineHeight = control.FindDefaultLineHeight(location, length);

                var yaxis = startRect.Top;
                var lineHeights = new List<double>();

                while ((endRect.Bottom - yaxis) > 0.001)
                {
                    double lineHeight;
                    if (yaxis == startRect.Top) // First Line
                    {
                        lineHeight = startRect.Bottom - startRect.Top;
                    }
                    else if (yaxis != endRect.Top) // Middle Line(s)
                    {
                        lineHeight = defaultLineHeight;
                    }
                    else // Bottom Line
                    {
                        lineHeight = endRect.Bottom - endRect.Top;
                    }
                    lineHeights.Add(lineHeight);
                    yaxis += (float)lineHeight;
                }

                ((ISpatialElement)span).Region = Region.FromLines(lineHeights.ToArray(), finalSize.Width, startRect.X, endRect.X, startRect.Top).Inflate(10);

                // update current location
                currentLocation += length;
            }
        }

        static CGRect GetCharacterBounds(NSRange characterRange, NSLayoutManager layoutManager, NSTextContainer textContainer)
        {
            var glyphRange = new NSRange();

            layoutManager.GetCharacterRange(characterRange, out glyphRange);

            return layoutManager.GetBoundingRect(glyphRange, textContainer);
        }

        static double FindDefaultLineHeight(this NativeLabel control, int start, int length)
        {
            if (length == 0)
                return 0.0;

            var textStorage = new NSTextStorage();

            textStorage.SetString(control.AttributedText.Substring(start, length));

            var layoutManager = new NSLayoutManager();
            textStorage.AddLayoutManager(layoutManager);

            var textContainer = new NSTextContainer(size: new SizeF(float.MaxValue, float.MaxValue))
            {
                LineFragmentPadding = 0
            };
            layoutManager.AddTextContainer(textContainer);

            var rect = GetCharacterBounds(new NSRange(0, 1), layoutManager, textContainer);
            return rect.Bottom - rect.Top;
        }

    }
}

@RedZone908
Copy link

@psatheesh18 yep, that's basically the same code @artemvalieiev shared above, the advance version of the fix that will eventually be applied officially in apparently .NET 8 SR3

@nmanocha
Copy link

Does someone has a fix for the Windows app when working with Label Spans hyperlink? This #17731 never got merged

@nicop85
Copy link

nicop85 commented Feb 15, 2024

Are you really closing this issue without fixing the Windows part? At least give a response to the users and explain the reasons behind the decision...

@mattleibow mattleibow reopened this Feb 15, 2024
@mattleibow
Copy link
Member

@nicop85 sorry about that, the bot is very persistent. But, it better watch out because we will replace it with AI.

@nicop85
Copy link

nicop85 commented Feb 15, 2024

I have found a workaround for Windows (#17731 ). Based on the changes made in that issue and the response of @artemvalieiev, I've played around to finally get to something that works. It is not perfect and there are some scenarios where the triggering area is bigger than expected (specially if the clickable span has a line break), but at least for me its better than something that doesn't do anything :)

In the MauiProgram.cs you will have to add

.ConfigureMauiHandlers((handlers) =>
{
#if WINDOWS
	handlers.AddHandler<Label, LabelHandlerFix17731>();
#endif
})

And then add this class to your project

using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Controls.Platform;
using Microsoft.Maui.Handlers;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Documents;

namespace OncoLogyc.Helpers
{
    /// <summary>
    /// Provides calculations for spatial regions of a <see cref="Label"/> that has a <see cref="Label.FormattedText"/> property set.
    /// </summary>
    public class LabelHandlerFix17731
        : LabelHandler
    {
        public LabelHandlerFix17731()
        {
        }

        public LabelHandlerFix17731(PropertyMapper mapper) : base(mapper)
        {
        }

        public LabelHandlerFix17731(PropertyMapper mapper, CommandMapper commandMapper) : base(mapper, commandMapper)
        {
        }

        public override void PlatformArrange(Rect rect)
        {
            base.PlatformArrange(rect);
            RecalculateSpanPositions(this);
        }

        static void RecalculateSpanPositions(LabelHandler labelHandler)
        {
            if (labelHandler.PlatformView is not TextBlock platformView || labelHandler.VirtualView is not Label virtualView)
                return;

            RecalculateSpanPositions(platformView, virtualView);
        }

        public static void RecalculateSpanPositions(TextBlock control, Label element)
        {
            if (element == null)
                return;

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

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

            var labelWidth = control.ActualWidth;

            if (labelWidth <= 0 || control.Height <= 0)
                return;

            var inlines = control.Inlines;

            if (inlines is null)
                return;

            var inlineHeights = GetInlineHeights(element);

            for (int i = 0; i < element.FormattedText.Spans.Count; i++)
            {
                var span = element.FormattedText.Spans[i];

                var length = span.Text?.Length ?? 0;

                if (length == 0)
                    continue;

                var inline = control.Inlines.ElementAt(i);
                var startRect = inline.ContentStart.GetCharacterRect(LogicalDirection.Forward);
                var endRect = inline.ContentEnd.GetCharacterRect(LogicalDirection.Backward);

                var defaultLineHeight = inlineHeights[i];
                var yaxis = startRect.Top;

                var lineHeights = new List<double>();

                while (yaxis < endRect.Bottom)
                {
                    double lineHeight;

                    if (yaxis == startRect.Top) // First Line
                    {
                        lineHeight = startRect.Bottom - startRect.Top;
                    }
                    else if (yaxis != endRect.Top) // Middle Line(s)
                    {
                        lineHeight = defaultLineHeight;
                    }
                    else // Bottom Line
                    {
                        lineHeight = endRect.Bottom - endRect.Top;
                    }

                    lineHeights.Add(lineHeight);
                    yaxis += lineHeight;
                }

                ((ISpatialElement)span).Region = Region.FromLines(lineHeights.ToArray(), labelWidth, startRect.X, endRect.X + endRect.Width, startRect.Top);
            }
        }

        static IList<double> GetInlineHeights(Label element)
        {
            IList<double> inlineHeights = new List<double>();

            if (element is not null)
            {
                FormattedString formatted = element.FormattedText;

                if (formatted is not null)
                {
                    var fontManager = element.Handler?.MauiContext?.Services?.GetRequiredService<IFontManager>();
                    
                    var heights = new List<double>();
                    for (var i = 0; i < formatted.Spans.Count; i++)
                    {
                        var span = formatted.Spans[i];

                        var run = span.ToRunAndColorsTuple(fontManager).Item1;

                        heights.Add(FindDefaultLineHeight(run));
                    }

                    inlineHeights = heights;
                }
            }

            return inlineHeights;
        }

        static double FindDefaultLineHeight(Inline inline)
        {
            var control = new TextBlock();

            control.Inlines.Add(inline);

            control.Measure(new Windows.Foundation.Size(double.PositiveInfinity, double.PositiveInfinity));

            var height = control.DesiredSize.Height;

            control.Inlines.Remove(inline);

            return height;
        }
    }
}

Hope it helps anyone and let me know if you notice something wrong with the code

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
area/controls 🎮 Label, Button, CheckBox, Slider, Stepper, Switch, Picker, Entry, Editor area/gestures control-label Label, Span delighter fixed-in-8.0.0-preview.5.8529 Look for this fix in 8.0.0-preview.5.8529! fixed-in-8.0.10 fixed-in-8.0.14 fixed-in-9.0.0-preview.2.10247 fixed-in-9.0.0-preview.2.10293 p/1 Work that is critical for the release, but we could probably ship without partner/cat 😻 Client CAT Team platform/android 🤖 platform/windows 🪟 t/bug Something isn't working
Projects
Status: Done
Triage
Ready For Work