FluentAvalonia
FluentAvalonia copied to clipboard
Soften Button Border Brushes
Describe the bug
Fluent v2 uses Acrylic Brushes for various borders -- including Button. Avalonia does not currently support Acrylic. Therefore, my assumption is FluentAvalonia uses the fallback values.
This results in button borders that are "too sharp" in dark theme (likely light as well).
To fix this, I would like to pre-blend the button border brush with the background brush taking into account the acrylic transparency as if it was there. This pre-blended color value would be the new border brush value. This means it would differ from WinUI numerically but look more visually similar (softening the button edges from a design standpoint).
Screenshots
WinUI (using UWP, WinUI 2 Library, Windows 10 Pro):
FluentAvalonia:
Desktop/Platform (please complete the following information):
- OS: [e.g. Windows (11/10), linux (distro), osx, wasm, etc]
- FluentAvalonia Version [e.g. 1.0.0, or latest]
- Avalonia Version [e.g. 0.10.10]
Additional context
I will fix this once direction is agreed.
Actually, Buttons don't use an acrylic brush. IIRC in Fluent v2, Acrylic brushes are only used in overlays (FlyoutPresenter, e.g.).
ButtonBorderBrush
maps to ControlElevationBorderBrush
which is a LinearGradientBrush composed of ControlStrokeColorSecondary
and ControlStrokeColorDefault
colors
I think the actual issue here is the use of BackgroundSizing=InnerBorderEdge
in WinUI, which isn't available in Avalonia. A quick test modifying the template to place the ContentPresenter in FABorder
with the background sizing set results in this, which looks much better.
Yea, sorry, I should have looked at the XAML and instead just made a guess. What you found makes sense though and the TestButton looks perfect. The good news is this is a lot "cleaner" to fix than customizing a bunch of colors.
How do you want to go about this change? Would you like me to open a PR switching to FABorder in all related control templates? This affects ComboBox, CalendarDatePicker, etc. as well.
I haven't looked at your FABorder implementation but I'm assuming it's fine for use in places like this.
I think the best solution to this is to get BackgroundSizing
added upstream to Border
and ContentPresenter
(and maybe TemplatedControl
, which would match all the UWP equivalents). I'd rather not add the FABorder to all controls (that need it) because:
1- While I don't really care about diverging templates from WinUI, I do prefer to keep things as simple as possible and keep the VisualTree as compressed as possible. And since this is a minor visual issue (at least to me), I don't view the extra template item worth it
2- FABorder isn't perfect in how it renders, particularly when there's a corner radius involved:
This is an extreme example with the contrasting colors (and zoomed in) and is a bit more subtle with the softer fluent colors, but highlights the slightly incorrect radii that doesn't match between the border and background geometry. An official implementation would need to fix this (and since geometry and me don't get along, it's not something I'm looking to do)
I think the best solution to this is to get BackgroundSizing added upstream
I've been thinking about this for a few months. Unfortunatley I don't think it's straightforward at all.
- In Windows, I think GDI and direct X just handles this behind the scenes. There is no need do anything but pass the properties down into the renderer.
- In Skia itself, this is NOT supported. Strokes are drawn on the edge of the shape itself. This means half the stroke extends past the shape and half the stroke is inside the shape. It's right on the middle. Fundamentally, THIS is the issue. I have no idea how Skia gets away with this since the shape bounds (width/height) I don't think take into account the stroke width... I have to look into that more.
- Since this isn't in Skia, how can it be added to Avalonia? Well, we have to do SEPARATE geometries for the shape fill (background) and then the shape stroke (border). These are complex geometries now taking into account different thicknesses and corner radius.
- Since we have to use complex geometry to get this to work... performance is a big question. This is low level how all of Avalonia is composed so if we stop using rectangles for everything and now have geometries I have to think performance is going to be noticeably worse.
- Now we have the composition renderer and there was an attempt to fix a few things with corner radius by moving calculations down into the composition system. That still has issues https://github.com/AvaloniaUI/Avalonia/pull/9488. There may be more tricks used there so I need to look at that code some more.
Bottom line, as you know, this is not an easy fix and goes pretty deep. If it was easier I'm sure it would have been done by now. I will open an issue and start poking around with things upstream though. This has to get solved sometime and 11.0 is really the best time.
- In Skia itself, this is NOT supported. Strokes are drawn on the edge of the shape itself. This means half the stroke extends past the shape and half the stroke is inside the shape. It's right on the middle. Fundamentally, THIS is the issue. I have no idea how Skia gets away with this since the shape bounds (width/height) I don't think take into account the stroke width... I have to look into that more.
Yes, that's is the issue - specifically its that Skia allows a combined drawing operation - which makes it very easy to overlook the possible issues of not adjusting the geometry between the fill & draw.
- In Windows, I think GDI and direct X just handles this behind the scenes. There is no need do anything but pass the properties down into the renderer.
Direct2D (and I believe GDI too) doesn't do "combined" draw calls like Skia does, you have to render the stroke and fill with 2 separate draw calls:
m_d2dContext.FillRectangle(&rect, pbrush);
m_d2dContext.DrawRectangle(&rect, pbrush);
However, if you don't adjust the geometry after Fill, you end up getting the Skia default equivalent:
- Since this isn't in Skia, how can it be added to Avalonia? Well, we have to do SEPARATE geometries for the shape fill (background) and then the shape stroke (border). These are complex geometries now taking into account different thicknesses and corner radius.
BorderRenderHelper
class in Avalonia uses the complex render path (2 geometries) already if:
- Non-uniform border thickness is detected - as that's not supported in any drawing API afaik
- Non-uniform corner radii, if the platform doesn't support that
For the non-uniform corner radii: Skia's RoundedRectangle class supports 4 unique corners. In DirectX, D2D1_ROUNDED_RECT only supports RadiusX and RadiusY which means even in DirectX, complex geometry is needed for the 4 corners. So, even WinUI has to be using 2 geometries for this case, since Direct2D is what's under the hood of the compositor.
The other issue is that because of this, Avalonia's border rendering is different whether the simple or complex path is taken - the simple path uses Skia's default and the complex path is equivalent to InnerBorderSizing
(I think).
Obviously there's a perf impact in taking the complex render path, but I don't see anyway around this. With all the transparency in modern UI themes, drawing a stroke "centered" is obviously not the right approach. We'll probably need some benchmarks in a "complex-ish" UI to see how bad this would be, particularly on non-desktop devices Avalonia supports.
I wonder if this issue also explains this artifact:
(left - FluentAvalonia, right - Windows Settings)
@Leon99 Do you still your issue above with the v2.1.0 previews?