Browse Source

Merge branch 'master' into feature/fluent-progress-bar

pull/4074/head
danwalmsley 6 years ago
committed by GitHub
parent
commit
adad63fc5e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 32
      NOTICE.md
  2. 2
      build/Moq.props
  3. 1
      samples/ControlCatalog/MainView.xaml
  4. 7
      samples/ControlCatalog/Pages/BorderPage.xaml
  5. 79
      samples/ControlCatalog/Pages/ToggleSwitchPage.xaml
  6. 19
      samples/ControlCatalog/Pages/ToggleSwitchPage.xaml.cs
  7. 5
      src/Avalonia.Controls/Border.cs
  8. 68
      src/Avalonia.Controls/ScrollViewer.cs
  9. 33
      src/Avalonia.Controls/ToggleSwitch.cs
  10. 8
      src/Avalonia.Layout/ILayoutManager.cs
  11. 2
      src/Avalonia.Layout/ILayoutable.cs
  12. 13
      src/Avalonia.Layout/LayoutManager.cs
  13. 57
      src/Avalonia.Layout/Layoutable.cs
  14. 2
      src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj
  15. 6
      src/Avalonia.Visuals/Media/DrawingContext.cs
  16. 6
      src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs
  17. 6
      src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
  18. 14
      src/Avalonia.Visuals/Rendering/SceneGraph/ClipNode.cs
  19. 19
      src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  20. 5
      src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs
  21. 5
      src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
  22. 9
      src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
  23. 11
      src/Avalonia.Visuals/RoundedRect.cs
  24. 15
      src/Avalonia.Visuals/VisualTree/IVisualWithRoundRectClip.cs
  25. 6
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  26. 15
      src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
  27. 6
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  28. 18
      tests/Avalonia.Controls.UnitTests/GridTests.cs
  29. 25
      tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs
  30. 119
      tests/Avalonia.Layout.UnitTests/LayoutableTests.cs

32
NOTICE.md

@ -271,3 +271,35 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE
# Chromium
https://github.com/chromium/chromium
// Copyright 2015 The Chromium Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

2
build/Moq.props

@ -1,5 +1,5 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="Moq" Version="4.7.99" />
<PackageReference Include="Moq" Version="4.14.1" />
</ItemGroup>
</Project>

1
samples/ControlCatalog/MainView.xaml

@ -57,6 +57,7 @@
<TabItem Header="TabStrip"><pages:TabStripPage/></TabItem>
<TabItem Header="TextBox"><pages:TextBoxPage/></TabItem>
<TabItem Header="TextBlock"><pages:TextBlockPage/></TabItem>
<TabItem Header="ToggleSwitch"><pages:ToggleSwitchPage/></TabItem>
<TabItem Header="ToolTip"><pages:ToolTipPage/></TabItem>
<TabItem Header="TreeView"><pages:TreeViewPage/></TabItem>
<TabItem Header="Viewbox"><pages:ViewboxPage/></TabItem>

7
samples/ControlCatalog/Pages/BorderPage.xaml

@ -29,6 +29,13 @@
Padding="16">
<TextBlock>Rounded Corners</TextBlock>
</Border>
<Border BorderBrush="{DynamicResource ThemeAccentBrush2}" Width="100" Height="100"
BorderThickness="0"
Background="White"
CornerRadius="100" ClipToBounds="True">
<Image Source="/Assets/maple-leaf-888807_640.jpg" Stretch="UniformToFill" />
</Border>
<TextBlock Text="Border with Clipping" HorizontalAlignment="Center" />
</StackPanel>
</StackPanel>
</UserControl>

79
samples/ControlCatalog/Pages/ToggleSwitchPage.xaml

@ -0,0 +1,79 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.ToggleSwitchPage" Margin="5">
<StackPanel Width="500" HorizontalAlignment="Center">
<TextBlock Text="Simple ToggleSwitch" Classes="header"/>
<Border Classes="Thin">
<StackPanel>
<ToggleSwitch Margin="10"/>
<TextBox Text="&lt;ToggleSwitch/&gt;" Classes="CodeBox"/>
</StackPanel>
</Border>
<TextBlock Text="headered ToggleSwitch" Classes="header"/>
<Border Classes="Thin">
<StackPanel>
<ToggleSwitch Content="headered" IsChecked="true" Margin="10"/>
<TextBox Classes="CodeBox"
Text="&lt;ToggleSwitch&gt;headered&lt;/ToggleSwitch&gt;"/>
</StackPanel>
</Border>
<TextBlock Text="Custom content ToggleSwitch" Classes="header"/>
<Border Classes="Thin">
<StackPanel>
<ToggleSwitch Content="Custom"
OnContent="On"
OffContent="Off"
Margin="10"/>
<TextBox Text="&lt;ToggleSwitch Content=&quot;Custom&quot;
ContentOn=&quot;On&quot;
ContentOff=&quot;Off&quot; /&gt;"
Classes="CodeBox"/>
</StackPanel>
</Border>
<TextBlock Text="Image content ToggleSwitch" Classes="header"/>
<Border Classes="Thin">
<StackPanel>
<ToggleSwitch Content="Just Click!" Margin="10">
<ToggleSwitch.OnContent>
<Image Source="/Assets/hirsch-899118_640.jpg" Height="32"/>
</ToggleSwitch.OnContent>
<ToggleSwitch.OffContent>
<Image Source="/Assets/delicate-arch-896885_640.jpg" Height="32"/>
</ToggleSwitch.OffContent>
</ToggleSwitch>
</StackPanel>
</Border>
</StackPanel>
<UserControl.Styles >
<Style Selector="TextBox.CodeBox" >
<Setter Property="Padding" Value="10"/>
<Setter Property="IsReadOnly" Value="True"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="IsEnabled" Value="true"/>
</Style>
<Style Selector="TextBlock.header">
<Setter Property="FontSize" Value="18"/>
<Setter Property="Margin" Value="0 20 0 20"/>
</Style>
<Style Selector="Border.Thin">
<Setter Property="BorderBrush" Value="Gray"/>
<Setter Property="BorderThickness" Value="0.5"/>
<Setter Property="CornerRadius" Value="2"/>
</Style>
</UserControl.Styles>
</UserControl>

19
samples/ControlCatalog/Pages/ToggleSwitchPage.xaml.cs

@ -0,0 +1,19 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace ControlCatalog.Pages
{
public class ToggleSwitchPage : UserControl
{
public ToggleSwitchPage()
{
this.InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

5
src/Avalonia.Controls/Border.cs

@ -1,13 +1,14 @@
using Avalonia.Controls.Utils;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
/// <summary>
/// A control which decorates a child with a border and background.
/// </summary>
public partial class Border : Decorator
public partial class Border : Decorator, IVisualWithRoundRectClip
{
/// <summary>
/// Defines the <see cref="Background"/> property.
@ -129,5 +130,7 @@ namespace Avalonia.Controls
{
return LayoutHelper.ArrangeChild(Child, finalSize, Padding, BorderThickness);
}
public CornerRadius ClipToBoundsRadius => CornerRadius;
}
}

68
src/Avalonia.Controls/ScrollViewer.cs

@ -181,6 +181,9 @@ namespace Avalonia.Controls
private Size _extent;
private Vector _offset;
private Size _viewport;
private Size _oldExtent;
private Vector _oldOffset;
private Size _oldViewport;
private Size _largeChange;
private Size _smallChange = new Size(DefaultSmallChange, DefaultSmallChange);
@ -198,6 +201,7 @@ namespace Avalonia.Controls
/// </summary>
public ScrollViewer()
{
LayoutUpdated += OnLayoutUpdated;
}
/// <summary>
@ -221,11 +225,9 @@ namespace Avalonia.Controls
private set
{
var old = _extent;
if (SetAndRaise(ExtentProperty, ref _extent, value))
{
CalculatedPropertiesChanged(extentDelta: value - old);
CalculatedPropertiesChanged();
}
}
}
@ -242,13 +244,11 @@ namespace Avalonia.Controls
set
{
var old = _offset;
value = ValidateOffset(this, value);
if (SetAndRaise(OffsetProperty, ref _offset, value))
{
CalculatedPropertiesChanged(offsetDelta: value - old);
CalculatedPropertiesChanged();
}
}
}
@ -265,11 +265,9 @@ namespace Avalonia.Controls
private set
{
var old = _viewport;
if (SetAndRaise(ViewportProperty, ref _viewport, value))
{
CalculatedPropertiesChanged(viewportDelta: value - old);
CalculatedPropertiesChanged();
}
}
}
@ -581,10 +579,7 @@ namespace Avalonia.Controls
}
}
private void CalculatedPropertiesChanged(
Size extentDelta = default,
Vector offsetDelta = default,
Size viewportDelta = default)
private void CalculatedPropertiesChanged()
{
// Pass old values of 0 here because we don't have the old values at this point,
// and it shouldn't matter as only the template uses these properies.
@ -605,20 +600,6 @@ namespace Avalonia.Controls
SetAndRaise(SmallChangeProperty, ref _smallChange, new Size(DefaultSmallChange, DefaultSmallChange));
SetAndRaise(LargeChangeProperty, ref _largeChange, Viewport);
}
if (extentDelta != default || offsetDelta != default || viewportDelta != default)
{
using var route = BuildEventRoute(ScrollChangedEvent);
if (route.HasHandlers)
{
var e = new ScrollChangedEventArgs(
new Vector(extentDelta.Width, extentDelta.Height),
offsetDelta,
new Vector(viewportDelta.Width, viewportDelta.Height));
route.RaiseEvent(this, e);
}
}
}
protected override void OnKeyDown(KeyEventArgs e)
@ -634,5 +615,38 @@ namespace Avalonia.Controls
e.Handled = true;
}
}
/// <summary>
/// Called when a change in scrolling state is detected, such as a change in scroll
/// position, extent, or viewport size.
/// </summary>
/// <param name="e">The event args.</param>
/// <remarks>
/// If you override this method, call `base.OnScrollChanged(ScrollChangedEventArgs)` to
/// ensure that this event is raised.
/// </remarks>
protected virtual void OnScrollChanged(ScrollChangedEventArgs e)
{
RaiseEvent(e);
}
private void OnLayoutUpdated(object sender, EventArgs e) => RaiseScrollChanged();
private void RaiseScrollChanged()
{
var extentDelta = new Vector(Extent.Width - _oldExtent.Width, Extent.Height - _oldExtent.Height);
var offsetDelta = Offset - _oldOffset;
var viewportDelta = new Vector(Viewport.Width - _oldViewport.Width, Viewport.Height - _oldViewport.Height);
if (!extentDelta.NearlyEquals(default) || !offsetDelta.NearlyEquals(default) || !viewportDelta.NearlyEquals(default))
{
var e = new ScrollChangedEventArgs(extentDelta, offsetDelta, viewportDelta);
OnScrollChanged(e);
_oldExtent = Extent;
_oldOffset = Offset;
_oldViewport = Viewport;
}
}
}
}

33
src/Avalonia.Controls/ToggleSwitch.cs

@ -1,4 +1,5 @@
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.LogicalTree;
@ -57,6 +58,18 @@ namespace Avalonia.Controls
set { SetValue(OffContentProperty, value); }
}
public IContentPresenter OffContentPresenter
{
get;
private set;
}
public IContentPresenter OnContentPresenter
{
get;
private set;
}
/// <summary>
/// Gets or Sets the <see cref="IDataTemplate"/> used to display the <see cref="OffContent"/>.
/// </summary>
@ -100,6 +113,24 @@ namespace Avalonia.Controls
LogicalChildren.Add(newChild);
}
}
protected override bool RegisterContentPresenter(IContentPresenter presenter)
{
var result = base.RegisterContentPresenter(presenter);
if (presenter.Name == "Part_OnContentPresenter")
{
OnContentPresenter = presenter;
result = true;
}
else if (presenter.Name == "PART_OffContentPresenter")
{
OffContentPresenter = presenter;
result = true;
}
return result;
}
}
}

8
src/Avalonia.Layout/ILayoutManager.cs

@ -1,3 +1,6 @@
using System;
#nullable enable
namespace Avalonia.Layout
{
@ -6,6 +9,11 @@ namespace Avalonia.Layout
/// </summary>
public interface ILayoutManager
{
/// <summary>
/// Raised when the layout manager completes a layout pass.
/// </summary>
event EventHandler LayoutUpdated;
/// <summary>
/// Notifies the layout manager that a control requires a measure.
/// </summary>

2
src/Avalonia.Layout/ILayoutable.cs

@ -1,5 +1,7 @@
using Avalonia.VisualTree;
#nullable enable
namespace Avalonia.Layout
{
/// <summary>

13
src/Avalonia.Layout/LayoutManager.cs

@ -3,6 +3,8 @@ using System.Diagnostics;
using Avalonia.Logging;
using Avalonia.Threading;
#nullable enable
namespace Avalonia.Layout
{
/// <summary>
@ -21,10 +23,12 @@ namespace Avalonia.Layout
_executeLayoutPass = ExecuteLayoutPass;
}
public event EventHandler? LayoutUpdated;
/// <inheritdoc/>
public void InvalidateMeasure(ILayoutable control)
{
Contract.Requires<ArgumentNullException>(control != null);
control = control ?? throw new ArgumentNullException(nameof(control));
Dispatcher.UIThread.VerifyAccess();
if (!control.IsAttachedToVisualTree)
@ -45,7 +49,7 @@ namespace Avalonia.Layout
/// <inheritdoc/>
public void InvalidateArrange(ILayoutable control)
{
Contract.Requires<ArgumentNullException>(control != null);
control = control ?? throw new ArgumentNullException(nameof(control));
Dispatcher.UIThread.VerifyAccess();
if (!control.IsAttachedToVisualTree)
@ -73,7 +77,7 @@ namespace Avalonia.Layout
{
_running = true;
Stopwatch stopwatch = null;
Stopwatch? stopwatch = null;
const LogEventLevel timingLogLevel = LogEventLevel.Information;
bool captureTiming = Logger.IsEnabled(timingLogLevel);
@ -117,13 +121,14 @@ namespace Avalonia.Layout
if (captureTiming)
{
stopwatch.Stop();
stopwatch!.Stop();
Logger.TryGet(timingLogLevel)?.Log(LogArea.Layout, this, "Layout pass finished in {Time}", stopwatch.Elapsed);
}
}
_queued = false;
LayoutUpdated?.Invoke(this, EventArgs.Empty);
}
/// <inheritdoc/>

57
src/Avalonia.Layout/Layoutable.cs

@ -1,8 +1,9 @@
using System;
using Avalonia.Logging;
using Avalonia.Utilities;
using Avalonia.VisualTree;
#nullable enable
namespace Avalonia.Layout
{
/// <summary>
@ -131,6 +132,7 @@ namespace Avalonia.Layout
private bool _measuring;
private Size? _previousMeasure;
private Rect? _previousArrange;
private EventHandler? _layoutUpdated;
/// <summary>
/// Initializes static members of the <see cref="Layoutable"/> class.
@ -153,7 +155,28 @@ namespace Avalonia.Layout
/// <summary>
/// Occurs when a layout pass completes for the control.
/// </summary>
public event EventHandler LayoutUpdated;
public event EventHandler? LayoutUpdated
{
add
{
if (_layoutUpdated is null && VisualRoot is ILayoutRoot r)
{
r.LayoutManager.LayoutUpdated += LayoutManagedLayoutUpdated;
}
_layoutUpdated += value;
}
remove
{
_layoutUpdated -= value;
if (_layoutUpdated is null && VisualRoot is ILayoutRoot r)
{
r.LayoutManager.LayoutUpdated -= LayoutManagedLayoutUpdated;
}
}
}
/// <summary>
/// Gets or sets the width of the element.
@ -358,12 +381,9 @@ namespace Avalonia.Layout
IsArrangeValid = true;
ArrangeCore(rect);
_previousArrange = rect;
LayoutUpdated?.Invoke(this, EventArgs.Empty);
}
}
/// <summary>
/// Called by InvalidateMeasure
/// </summary>
@ -693,6 +713,26 @@ namespace Avalonia.Layout
InvalidateMeasure();
}
protected override void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTreeCore(e);
if (_layoutUpdated is object && e.Root is ILayoutRoot r)
{
r.LayoutManager.LayoutUpdated += LayoutManagedLayoutUpdated;
}
}
protected override void OnDetachedFromVisualTreeCore(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTreeCore(e);
if (_layoutUpdated is object && e.Root is ILayoutRoot r)
{
r.LayoutManager.LayoutUpdated -= LayoutManagedLayoutUpdated;
}
}
/// <inheritdoc/>
protected sealed override void OnVisualParentChanged(IVisual oldParent, IVisual newParent)
{
@ -701,6 +741,13 @@ namespace Avalonia.Layout
base.OnVisualParentChanged(oldParent, newParent);
}
/// <summary>
/// Called when the layout manager raises a LayoutUpdated event.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The event args.</param>
private void LayoutManagedLayoutUpdated(object sender, EventArgs e) => _layoutUpdated?.Invoke(this, e);
/// <summary>
/// Tests whether any of a <see cref="Rect"/>'s properties include negative values,
/// a NaN or Infinity.

2
src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj

@ -19,4 +19,4 @@
</ItemGroup>
<Import Project="..\..\build\BuildTargets.targets" />
<Import Project="..\..\build\Rx.props" />
</Project>
</Project>

6
src/Avalonia.Visuals/Media/DrawingContext.cs

@ -283,6 +283,12 @@ namespace Avalonia.Media
}
public PushedState PushClip(RoundedRect clip)
{
PlatformImpl.PushClip(clip);
return new PushedState(this, PushedState.PushedStateType.Clip);
}
/// <summary>
/// Pushes a clip rectangle.
/// </summary>

6
src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs

@ -107,6 +107,12 @@ namespace Avalonia.Platform
/// <param name="clip">The clip rectangle.</param>
void PushClip(Rect clip);
/// <summary>
/// Pushes a clip rounded rectangle.
/// </summary>
/// <param name="clip">The clip rounded rectangle</param>
void PushClip(RoundedRect clip);
/// <summary>
/// Pops the latest pushed clip rectangle.
/// </summary>

6
src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs

@ -289,7 +289,11 @@ namespace Avalonia.Rendering
using (context.PushPostTransform(m))
using (context.PushOpacity(opacity))
using (clipToBounds ? context.PushClip(bounds) : default(DrawingContext.PushedState))
using (clipToBounds
? visual is IVisualWithRoundRectClip roundClipVisual
? context.PushClip(new RoundedRect(bounds, roundClipVisual.ClipToBoundsRadius))
: context.PushClip(bounds)
: default(DrawingContext.PushedState))
using (visual.Clip != null ? context.PushGeometryClip(visual.Clip) : default(DrawingContext.PushedState))
using (visual.OpacityMask != null ? context.PushOpacityMask(visual.OpacityMask, bounds) : default(DrawingContext.PushedState))
using (context.PushTransformContainer())

14
src/Avalonia.Visuals/Rendering/SceneGraph/ClipNode.cs

@ -16,6 +16,16 @@ namespace Avalonia.Rendering.SceneGraph
{
Clip = clip;
}
/// <summary>
/// Initializes a new instance of the <see cref="ClipNode"/> class that represents a
/// clip push.
/// </summary>
/// <param name="clip">The clip to push.</param>
public ClipNode(RoundedRect clip)
{
Clip = clip;
}
/// <summary>
/// Initializes a new instance of the <see cref="ClipNode"/> class that represents a
@ -31,7 +41,7 @@ namespace Avalonia.Rendering.SceneGraph
/// <summary>
/// Gets the clip to be pushed or null if the operation represents a pop.
/// </summary>
public Rect? Clip { get; }
public RoundedRect? Clip { get; }
/// <inheritdoc/>
public bool HitTest(Point p) => false;
@ -45,7 +55,7 @@ namespace Avalonia.Rendering.SceneGraph
/// The properties of the other draw operation are passed in as arguments to prevent
/// allocation of a not-yet-constructed draw operation object.
/// </remarks>
public bool Equals(Rect? clip) => Clip == clip;
public bool Equals(RoundedRect? clip) => Clip == clip;
/// <inheritdoc/>
public void Render(IDrawingContextImpl context)

19
src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs

@ -163,7 +163,7 @@ namespace Avalonia.Rendering.SceneGraph
++_drawOperationindex;
}
}
public void Custom(ICustomDrawOperation custom)
{
var next = NextDrawAs<CustomDrawOperation>();
@ -283,6 +283,21 @@ namespace Avalonia.Rendering.SceneGraph
}
}
/// <inheritdoc />
public void PushClip(RoundedRect clip)
{
var next = NextDrawAs<ClipNode>();
if (next == null || !next.Item.Equals(clip))
{
Add(new ClipNode(clip));
}
else
{
++_drawOperationindex;
}
}
/// <inheritdoc/>
public void PushGeometryClip(IGeometryImpl clip)
{
@ -368,7 +383,7 @@ namespace Avalonia.Rendering.SceneGraph
{
using (var refCounted = RefCountable.Create(node))
{
Add(refCounted);
Add(refCounted);
}
}

5
src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs

@ -26,6 +26,11 @@ namespace Avalonia.Rendering.SceneGraph
/// </summary>
Matrix Transform { get; }
/// <summary>
/// Gets the corner radius of visual. Contents are clipped to this radius.
/// </summary>
CornerRadius ClipToBoundsRadius { get; }
/// <summary>
/// Gets the bounds of the node's geometry in global coordinates.
/// </summary>

5
src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs

@ -163,6 +163,10 @@ namespace Avalonia.Rendering.SceneGraph
var visual = node.Visual;
var opacity = visual.Opacity;
var clipToBounds = visual.ClipToBounds;
var clipToBoundsRadius = visual is IVisualWithRoundRectClip roundRectClip ?
roundRectClip.ClipToBoundsRadius :
default;
var bounds = new Rect(visual.Bounds.Size);
var contextImpl = (DeferredDrawingContextImpl)context.PlatformImpl;
@ -201,6 +205,7 @@ namespace Avalonia.Rendering.SceneGraph
node.ClipBounds = clipBounds;
node.ClipToBounds = clipToBounds;
node.LayoutBounds = globalBounds;
node.ClipToBoundsRadius = clipToBoundsRadius;
node.GeometryClip = visual.Clip?.PlatformImpl;
node.Opacity = opacity;

9
src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs

@ -46,6 +46,9 @@ namespace Avalonia.Rendering.SceneGraph
/// <inheritdoc/>
public IVisualNode Parent { get; }
/// <inheritdoc/>
public CornerRadius ClipToBoundsRadius { get; set; }
/// <inheritdoc/>
public Matrix Transform { get; set; }
@ -262,6 +265,7 @@ namespace Avalonia.Rendering.SceneGraph
{
Transform = Transform,
ClipBounds = ClipBounds,
ClipToBoundsRadius = ClipToBoundsRadius,
ClipToBounds = ClipToBounds,
LayoutBounds = LayoutBounds,
GeometryClip = GeometryClip,
@ -301,7 +305,10 @@ namespace Avalonia.Rendering.SceneGraph
if (ClipToBounds)
{
context.Transform = Matrix.Identity;
context.PushClip(ClipBounds);
if (ClipToBoundsRadius.IsEmpty)
context.PushClip(ClipBounds);
else
context.PushClip(new RoundedRect(ClipBounds, ClipToBoundsRadius));
}
context.Transform = Transform;

11
src/Avalonia.Visuals/RoundedRect.cs

@ -27,6 +27,10 @@ namespace Avalonia
}
}
public static bool operator ==(RoundedRect left, RoundedRect right) => left.Equals(right);
public static bool operator !=(RoundedRect left, RoundedRect right) => !left.Equals(right);
public Rect Rect { get; }
public Vector RadiiTopLeft { get; }
public Vector RadiiTopRight { get; }
@ -74,6 +78,13 @@ namespace Avalonia
}
public RoundedRect(in Rect bounds, in CornerRadius radius) : this(bounds,
radius.TopLeft, radius.TopRight,
radius.BottomRight, radius.BottomLeft)
{
}
public static implicit operator RoundedRect(Rect r) => new RoundedRect(r);
public bool IsRounded => RadiiTopLeft != default || RadiiTopRight != default || RadiiBottomRight != default ||

15
src/Avalonia.Visuals/VisualTree/IVisualWithRoundRectClip.cs

@ -0,0 +1,15 @@
using System;
namespace Avalonia.VisualTree
{
[Obsolete("Internal API, will be removed in future versions, you've been warned")]
public interface IVisualWithRoundRectClip
{
/// <summary>
/// Gets a value indicating the corner radius of control's clip bounds
/// </summary>
[Obsolete("Internal API, will be removed in future versions, you've been warned")]
CornerRadius ClipToBoundsRadius { get; }
}
}

6
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -389,6 +389,12 @@ namespace Avalonia.Skia
Canvas.ClipRect(clip.ToSKRect());
}
public void PushClip(RoundedRect clip)
{
Canvas.Save();
Canvas.ClipRoundRect(clip.ToSKRoundRect());
}
/// <inheritdoc />
public void PopClip()
{

15
src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs

@ -40,6 +40,21 @@ namespace Avalonia.Skia
return new SKRect((float)r.X, (float)r.Y, (float)r.Right, (float)r.Bottom);
}
public static SKRoundRect ToSKRoundRect(this RoundedRect r)
{
var rc = r.Rect.ToSKRect();
var result = new SKRoundRect();
result.SetRectRadii(rc,
new[]
{
r.RadiiTopLeft.ToSKPoint(), r.RadiiTopRight.ToSKPoint(),
r.RadiiBottomRight.ToSKPoint(), r.RadiiBottomLeft.ToSKPoint(),
});
return result;
}
public static Rect ToAvaloniaRect(this SKRect r)
{
return new Rect(r.Left, r.Top, r.Right - r.Left, r.Bottom - r.Top);

6
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@ -359,6 +359,12 @@ namespace Avalonia.Direct2D1.Media
_deviceContext.PushAxisAlignedClip(clip.ToSharpDX(), AntialiasMode.PerPrimitive);
}
public void PushClip(RoundedRect clip)
{
//TODO: radius
_deviceContext.PushAxisAlignedClip(clip.Rect.ToDirect2D(), AntialiasMode.PerPrimitive);
}
public void PopClip()
{
_deviceContext.PopAxisAlignedClip();

18
tests/Avalonia.Controls.UnitTests/GridTests.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.UnitTests;
using Xunit;
using Xunit.Abstractions;
@ -1182,13 +1183,18 @@ namespace Avalonia.Controls.UnitTests
foreach (var xgrids in grids)
scope.Children.Add(xgrids);
var root = new Grid();
root.UseLayoutRounding = false;
root.SetValue(Grid.IsSharedSizeScopeProperty, true);
root.Children.Add(scope);
var rootGrid = new Grid();
rootGrid.UseLayoutRounding = false;
rootGrid.SetValue(Grid.IsSharedSizeScopeProperty, true);
rootGrid.Children.Add(scope);
root.Measure(new Size(50, 50));
root.Arrange(new Rect(new Point(), new Point(50, 50)));
var root = new TestRoot(rootGrid)
{
Width = 50,
Height = 50,
};
root.LayoutManager.ExecuteInitialLayoutPass(root);
PrintColumnDefinitions(grids[0]);
Assert.Equal(5, grids[0].ColumnDefinitions[0].ActualWidth);

25
tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs

@ -4,6 +4,7 @@ using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Layout;
using Avalonia.UnitTests;
using Moq;
using Xunit;
@ -150,12 +151,15 @@ namespace Avalonia.Controls.UnitTests
public void Changing_Extent_Should_Raise_ScrollChanged()
{
var target = new ScrollViewer();
var root = new TestRoot(target);
var raised = 0;
target.SetValue(ScrollViewer.ExtentProperty, new Size(100, 100));
target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50));
target.Offset = new Vector(10, 10);
root.LayoutManager.ExecuteInitialLayoutPass(root);
target.ScrollChanged += (s, e) =>
{
Assert.Equal(new Vector(11, 12), e.ExtentDelta);
@ -166,20 +170,26 @@ namespace Avalonia.Controls.UnitTests
target.SetValue(ScrollViewer.ExtentProperty, new Size(111, 112));
Assert.Equal(1, raised);
Assert.Equal(0, raised);
root.LayoutManager.ExecuteLayoutPass();
Assert.Equal(1, raised);
}
[Fact]
public void Changing_Offset_Should_Raise_ScrollChanged()
{
var target = new ScrollViewer();
var root = new TestRoot(target);
var raised = 0;
target.SetValue(ScrollViewer.ExtentProperty, new Size(100, 100));
target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50));
target.Offset = new Vector(10, 10);
root.LayoutManager.ExecuteInitialLayoutPass(root);
target.ScrollChanged += (s, e) =>
{
Assert.Equal(default, e.ExtentDelta);
@ -190,20 +200,26 @@ namespace Avalonia.Controls.UnitTests
target.Offset = new Vector(22, 24);
Assert.Equal(1, raised);
Assert.Equal(0, raised);
root.LayoutManager.ExecuteLayoutPass();
Assert.Equal(1, raised);
}
[Fact]
public void Changing_Viewport_Should_Raise_ScrollChanged()
{
var target = new ScrollViewer();
var root = new TestRoot(target);
var raised = 0;
target.SetValue(ScrollViewer.ExtentProperty, new Size(100, 100));
target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50));
target.Offset = new Vector(10, 10);
root.LayoutManager.ExecuteInitialLayoutPass(root);
target.ScrollChanged += (s, e) =>
{
Assert.Equal(default, e.ExtentDelta);
@ -214,8 +230,11 @@ namespace Avalonia.Controls.UnitTests
target.SetValue(ScrollViewer.ViewportProperty, new Size(56, 58));
Assert.Equal(1, raised);
Assert.Equal(0, raised);
root.LayoutManager.ExecuteLayoutPass();
Assert.Equal(1, raised);
}
private Control CreateTemplate(ScrollViewer control, INameScope scope)

119
tests/Avalonia.Layout.UnitTests/LayoutableTests.cs

@ -203,6 +203,125 @@ namespace Avalonia.Layout.UnitTests
Assert.Equal(new Rect(expectedX, 0, childWidth, 100), target.Bounds);
}
[Fact]
public void LayoutUpdated_Is_Called_At_End_Of_Layout_Pass()
{
Border border1;
Border border2;
var layoutManager = new LayoutManager();
var root = new TestRoot
{
Child = border1 = new Border
{
Child = border2 = new Border(),
},
LayoutManager = layoutManager,
};
var raised = 0;
void ValidateBounds(object sender, EventArgs e)
{
Assert.Equal(new Rect(0, 0, 100, 100), border1.Bounds);
Assert.Equal(new Rect(0, 0, 100, 100), border2.Bounds);
++raised;
}
root.LayoutUpdated += ValidateBounds;
border1.LayoutUpdated += ValidateBounds;
border2.LayoutUpdated += ValidateBounds;
root.Measure(new Size(100, 100));
root.Arrange(new Rect(0, 0, 100, 100));
layoutManager.ExecuteLayoutPass();
Assert.Equal(3, raised);
Assert.Equal(new Rect(0, 0, 100, 100), border1.Bounds);
Assert.Equal(new Rect(0, 0, 100, 100), border2.Bounds);
}
[Fact]
public void LayoutUpdated_Subscribes_To_LayoutManager()
{
Border target;
var layoutManager = new Mock<ILayoutManager>();
layoutManager.SetupAdd(m => m.LayoutUpdated += (sender, args) => { });
var root = new TestRoot
{
Child = new Border
{
Child = target = new Border(),
},
LayoutManager = layoutManager.Object,
};
void Handler(object sender, EventArgs e) {}
layoutManager.Invocations.Clear();
target.LayoutUpdated += Handler;
layoutManager.VerifyAdd(
x => x.LayoutUpdated += It.IsAny<EventHandler>(),
Times.Once);
layoutManager.Invocations.Clear();
target.LayoutUpdated -= Handler;
layoutManager.VerifyRemove(
x => x.LayoutUpdated -= It.IsAny<EventHandler>(),
Times.Once);
}
[Fact]
public void LayoutManager_LayoutUpdated_Is_Subscribed_When_Attached_To_Tree()
{
Border border1;
var layoutManager = new Mock<ILayoutManager>();
layoutManager.SetupAdd(m => m.LayoutUpdated += (sender, args) => { });
var root = new TestRoot
{
Child = border1 = new Border(),
LayoutManager = layoutManager.Object,
};
var border2 = new Border();
border2.LayoutUpdated += (s, e) => { };
layoutManager.Invocations.Clear();
border1.Child = border2;
layoutManager.VerifyAdd(
x => x.LayoutUpdated += It.IsAny<EventHandler>(),
Times.Once);
}
[Fact]
public void LayoutManager_LayoutUpdated_Is_Unsubscribed_When_Detached_From_Tree()
{
Border border1;
var layoutManager = new Mock<ILayoutManager>();
layoutManager.SetupAdd(m => m.LayoutUpdated += (sender, args) => { });
var root = new TestRoot
{
Child = border1 = new Border(),
LayoutManager = layoutManager.Object,
};
var border2 = new Border();
border2.LayoutUpdated += (s, e) => { };
border1.Child = border2;
layoutManager.Invocations.Clear();
border1.Child = null;
layoutManager.VerifyRemove(
x => x.LayoutUpdated -= It.IsAny<EventHandler>(),
Times.Once);
}
private class TestLayoutable : Layoutable
{
public Size ArrangeSize { get; private set; }

Loading…
Cancel
Save