dotnet / maui

.NET MAUI is the .NET Multi-platform App UI, a framework for building native device applications spanning mobile, tablet, and desktop.
https://dot.net/maui
MIT License
22.24k stars 1.76k forks source link

Border's corner radius applied to child's top left corner in Windows, iOS and MacCatalyst #16444

Closed satzmuthu closed 1 year ago

satzmuthu commented 1 year ago

Description

Parent border's corner radius is applied to first child's(border/rounded rectangle) top left corner

Steps to Reproduce

Just copy the following code and execute `<Border StrokeShape="RoundRectangle 12" Stroke="blue" BackgroundColor="LightBlue" Padding="16">

        </Border>`

Border_Corner_Radius

Link to public reproduction project repository

https://github.com/satzmuthu/TestMAUI

Version with bug

7.0.49

Last version that worked well

Unknown/Other

Affected platforms

iOS, Windows, macOS

Affected platform versions

iOS 16, Window 10.0.19041.0

Did you find any workaround?

Used frame instead of parent border

Relevant log output

No response

XamlTest commented 1 year ago

Verified this on Visual Studio Enterprise 17.8.0 Preview 1.0. Repro on Windows 11 and iOS 16.4 .NET 8, not repro on Android 13.0-API33 with below Project: 16444.zip

Axemasta commented 1 year ago

This has been an annoying issue on net6/7 but I believe it is fixed in net8 (it was the last time I tried).

In the meantime I have a quick and dirty hack for ios / mac to fix the issue. On those platforms its caused by a mask being applied to the first child view of the border UIView. Here is a behavior to remove the mask from that view, it will likely cause other knock on effects which is why I'd call it hacky but it works for my cases and hopefully should get people actually be able to put content in a rounded rectangle in maui... 🤦‍♂️

Shared behavior

namespace YourApp.Behaviors;

public partial class FixBorderRadiusEffect : PlatformBehavior<View>
{
}

iOS / Mac Implementation

Depending on your compile setup you'll need to either place this in both iOS & Mac platform folders, if you have mac + ios multitargetting you'll only need this once


using UIKit;

namespace YourApp.Behaviors;

public partial class FixBorderRadiusEffect { protected override void OnAttachedTo(View bindable, UIView platformView) { if (platformView.Layer.Mask != null) { platformView.Layer.Mask = null; } }

protected override void OnDetachedFrom(View bindable, UIView platformView)
{
    base.OnDetachedFrom(bindable, platformView);
}

}

jstedfast commented 1 year ago

My understanding is that the rounded inner bounds is the desired effect - at least, that's the effect I've been making sure happens as I've been fixing border issues on the various platforms.

It's also how WinUI works.

In my opinion, this is not a bug.

Axemasta commented 1 year ago

@jstedfast either way you cut this there is a bug, I strongly disagree corner radius should work in the way you described on iOS and Mac, especially since it never behaved this way in Forms.

Assuming that the CornerRadius applied to a Border should then apply to any view houses within the Border, in this example only the top left corner has a radius applied. TR, BR, BL corners all have no radius so given this was the correct way to implement radius, those aren't applying.

But here I would strongly argue corner radius should not behave in this manor, it should clip content that overflows the radius corners but not affect child views.

Consider this example in Forms with a Frame:

<Frame BackgroundColor="#2196F3" Padding="20" CornerRadius="20">
    <Label Text="Corner Radius Clipping" HorizontalTextAlignment="Center" TextColor="White" FontSize="36" BackgroundColor="Orange"/>
</Frame>

You would get this view:

image

The border equivalent in maui of this would be:

<Border StrokeThickness="0" StrokeShape="RoundRectangle 20" BackgroundColor="#2196F3" Padding="20">
    <Label Text="Corner Radius Clipping" HorizontalTextAlignment="Center" TextColor="White" FontSize="36" BackgroundColor="Orange"/>
</Border>

And you would see:

image

If the radius should be applied to child views it would render as: image

But from a developer experience, why would we want the corner radius of a parent container applying to its children? This example uses a Label, but this corner mask applies to any view including layout views. If this is a feature, its really annoying and will be frustrating for developers because rounded corner designs are popular and this wasn't an issue in Forms and its never been an issue on any other UI framework. If the contents of the child view gets cut off by the corner radius, some way of specifying whether the view should clip its bounds would be nice (Frame already had this).

This issue mentions windows, mac, ios. If this is the behaviour of WinUI then the native behaviour should be preserved. However this is absolutely not the native behaviour of mac / iOS, if you wanted the inner clipping on inner views you would have to explicitly apply it.

jstedfast commented 1 year ago

And you would see:

What you are seeing from MAUI right now is definitely a bug and I'm pretty sure it's (partially) fixed by this PR: https://github.com/dotnet/maui/pull/17310

If the radius should be applied to child views it would render as:

Right, which is the bug and is fixed for Ellipse, Rectangle and RoundRectangle border shapes in the aforementioned PR.

Polygon is still broken and Image clipping is still broken on Windows (and I just filed a new bug for that).

durandt commented 1 year ago

This has been an annoying issue on net6/7 but I believe it is fixed in net8 (it was the last time I tried).

In the meantime I have a quick and dirty hack for ios / mac to fix the issue. On those platforms its caused by a mask being applied to the first child view of the border UIView. Here is a behavior to remove the mask from that view, it will likely cause other knock on effects which is why I'd call it hacky but it works for my cases and hopefully should get people actually be able to put content in a rounded rectangle in maui... 🤦‍♂️

Shared behavior

namespace YourApp.Behaviors;

public partial class FixBorderRadiusEffect : PlatformBehavior<View>
{
}

iOS / Mac Implementation

Depending on your compile setup you'll need to either place this in both iOS & Mac platform folders, if you have mac + ios multitargetting you'll only need this once

using UIKit;

namespace YourApp.Behaviors;

public partial class FixBorderRadiusEffect
{
  protected override void OnAttachedTo(View bindable, UIView platformView)
  {
      if (platformView.Layer.Mask != null)
      {
          platformView.Layer.Mask = null;
      }
  }

  protected override void OnDetachedFrom(View bindable, UIView platformView)
  {
      base.OnDetachedFrom(bindable, platformView);
  }
}

@Axemasta

Based on your description (and my tests) I guess you meant:

public partial class FixBorderRadiusEffect
{
    protected override void OnAttachedTo(View bindable, UIView platformView)
    {
        if (platformView.Subviews.Any())
        {
            var firstSubView = platformView.Subviews.First();
            if (firstSubView.Layer.Mask != null)
            {
                firstSubView.Layer.Mask = null;
            }
        }
    }
}
ghost commented 1 year ago

Hello lovely human, thank you for your comment on this issue. Because this issue has been closed for a period of time, please strongly consider opening a new issue linking to this issue instead to ensure better visibility of your comment. Thank you!

Axemasta commented 1 year ago

@durandt

Based on your description (and my tests) I guess you meant:

No, my above solution required the behavior to be placed on the first child view of the border:

<Border>
    <VerticalStackLayout>
        <VerticalStackLayout.Behaviors>
            <fixes:FixBorderRadiusEffect/>
        </VerticalStackLayout.Behaviors>
        <Label/>
        <Button/>
    </VerticalStackLayout>
</Border>

Obviously thats a little cumbersome, the implementation I am using in my net7 project is functionally the same as yours:

public partial class FixBorderRadiusBehavior
{
    protected override void OnAttachedTo(View bindable, UIView platformView)
    {
        var firstSubView = platformView.Subviews.FirstOrDefault();

        if (firstSubView is null)
        {
            return;
        }

        if (firstSubView.Layer.Mask != null)
        {
            firstSubView.Layer.Mask = null;
        }
    }
}

And I add it to the border handler mapper to every border in my app:

mauiAppBuilder.ConfigureMauiHandlers(handlers =>
{
#if IOS
    BorderHandler.Mapper.AppendToMapping("FixCorderRadius", (handler, view) =>
    {
        if (handler.VirtualView is Border border)
        {
            border.Behaviors.Add(new FixBorderRadiusBehavior());
        }
    });
#endif
});
durandt commented 1 year ago

Ah thank you for you quick reply and for sharing. There's always many ways to do the same thing :D