Browse Source

Merge branch 'master' into fixes/2315-addclasshandler-static

pull/2536/head
Steven Kirk 7 years ago
committed by GitHub
parent
commit
e93b4099a4
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .github/FUNDING.yml
  2. 2
      .gitignore
  3. 3
      .gitmodules
  4. 1
      Avalonia.sln
  5. 5
      dirs.proj
  6. 1
      native/Avalonia.Native/inc/avalonia-native.h
  7. 10
      native/Avalonia.Native/src/OSX/cursor.h
  8. 5
      native/Avalonia.Native/src/OSX/cursor.mm
  9. 10
      native/Avalonia.Native/src/OSX/window.mm
  10. 1
      packages/Avalonia/Avalonia.csproj
  11. 4
      readme.md
  12. 1
      samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
  13. 6
      samples/ControlCatalog.NetCore/Program.cs
  14. 1
      samples/ControlCatalog/MainView.xaml
  15. 4
      samples/ControlCatalog/Pages/DataGridPage.xaml
  16. 99
      samples/ControlCatalog/Pages/PointersPage.cs
  17. 4
      samples/ControlCatalog/Pages/TextBoxPage.xaml
  18. 3
      samples/ControlCatalog/ViewModels/MenuPageViewModel.cs
  19. 3
      samples/RenderDemo/MainWindow.xaml
  20. 49
      samples/RenderDemo/Pages/RenderTargetBitmapPage.cs
  21. 16
      src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs
  22. 119
      src/Avalonia.Base/Utilities/MathUtilities.cs
  23. 11
      src/Avalonia.Build.Tasks/Program.cs
  24. 32
      src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs
  25. 13
      src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
  26. 1
      src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj
  27. 25
      src/Avalonia.Controls.DataGrid/DataGridRow.cs
  28. 15
      src/Avalonia.Controls/Application.cs
  29. 30
      src/Avalonia.Controls/Button.cs
  30. 13
      src/Avalonia.Controls/Calendar/CalendarButton.cs
  31. 13
      src/Avalonia.Controls/Calendar/CalendarDayButton.cs
  32. 32
      src/Avalonia.Controls/Calendar/CalendarItem.cs
  33. 10
      src/Avalonia.Controls/ColumnDefinition.cs
  34. 7
      src/Avalonia.Controls/ColumnDefinitions.cs
  35. 2
      src/Avalonia.Controls/ComboBox.cs
  36. 732
      src/Avalonia.Controls/DefinitionBase.cs
  37. 60
      src/Avalonia.Controls/DefinitionList.cs
  38. 3482
      src/Avalonia.Controls/Grid.cs
  39. 54
      src/Avalonia.Controls/MenuItem.cs
  40. 4
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  41. 22
      src/Avalonia.Controls/Platform/InProcessDragSource.cs
  42. 69
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  43. 64
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  44. 4
      src/Avalonia.Controls/Primitives/Popup.cs
  45. 22
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  46. 2
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  47. 18
      src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs
  48. 16
      src/Avalonia.Controls/RowDefinition.cs
  49. 5
      src/Avalonia.Controls/RowDefinitions.cs
  50. 18
      src/Avalonia.Controls/TabControl.cs
  51. 56
      src/Avalonia.Controls/TextBox.cs
  52. 705
      src/Avalonia.Controls/Utils/GridLayout.cs
  53. 651
      src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs
  54. 14
      src/Avalonia.Controls/Window.cs
  55. 196
      src/Avalonia.Controls/WrapPanel.cs
  56. 6
      src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs
  57. 1
      src/Avalonia.Desktop/Avalonia.Desktop.csproj
  58. 1
      src/Avalonia.Input/Cursors.cs
  59. 2
      src/Avalonia.Input/FocusManager.cs
  60. 127
      src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs
  61. 23
      src/Avalonia.Input/GestureRecognizers/IGestureRecognizer.cs
  62. 183
      src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs
  63. 18
      src/Avalonia.Input/Gestures.cs
  64. 6
      src/Avalonia.Input/IInputElement.cs
  65. 5
      src/Avalonia.Input/IMouseDevice.cs
  66. 18
      src/Avalonia.Input/IPointer.cs
  67. 8
      src/Avalonia.Input/IPointerDevice.cs
  68. 120
      src/Avalonia.Input/InputElement.cs
  69. 2
      src/Avalonia.Input/InputExtensions.cs
  70. 227
      src/Avalonia.Input/MouseDevice.cs
  71. 4
      src/Avalonia.Input/Navigation/FocusExtensions.cs
  72. 73
      src/Avalonia.Input/Pointer.cs
  73. 108
      src/Avalonia.Input/PointerEventArgs.cs
  74. 45
      src/Avalonia.Input/PointerPoint.cs
  75. 12
      src/Avalonia.Input/PointerWheelEventArgs.cs
  76. 1
      src/Avalonia.Input/Properties/AssemblyInfo.cs
  77. 4
      src/Avalonia.Input/Raw/RawMouseWheelEventArgs.cs
  78. 15
      src/Avalonia.Input/Raw/RawPointerEventArgs.cs
  79. 15
      src/Avalonia.Input/Raw/RawTouchEventArgs.cs
  80. 29
      src/Avalonia.Input/ScrollGestureEventArgs.cs
  81. 74
      src/Avalonia.Input/TouchDevice.cs
  82. 4
      src/Avalonia.Native/Avalonia.Native.csproj
  83. 2
      src/Avalonia.Native/WindowImplBase.cs
  84. 18
      src/Avalonia.OpenGL/AngleOptions.cs
  85. 71
      src/Avalonia.OpenGL/EglDisplay.cs
  86. 33
      src/Avalonia.OpenGL/EglGlPlatformSurface.cs
  87. 43
      src/Avalonia.OpenGL/EglInterface.cs
  88. 7
      src/Avalonia.OpenGL/IGlPlatformSurfaceRenderTarget.cs
  89. 5
      src/Avalonia.ReactiveUI/AppBuilderExtensions.cs
  90. 55
      src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs
  91. 1
      src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj
  92. 143
      src/Avalonia.ReactiveUI/RoutedViewHost.cs
  93. 75
      src/Avalonia.ReactiveUI/TransitioningContentControl.cs
  94. 80
      src/Avalonia.ReactiveUI/ViewModelViewHost.cs
  95. 2
      src/Avalonia.Styling/Controls/IResourceProvider.cs
  96. 2
      src/Avalonia.Styling/Controls/ResourceDictionary.cs
  97. 10
      src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs
  98. 19
      src/Avalonia.Styling/StyledElement.cs
  99. 2
      src/Avalonia.Styling/Styling/Style.cs
  100. 2
      src/Avalonia.Styling/Styling/Styles.cs

1
.github/FUNDING.yml

@ -0,0 +1 @@
open_collective: avalonia

2
.gitignore

@ -196,3 +196,5 @@ ModuleCache.noindex/
Build/Intermediates.noindex/
info.plist
build-intermediate
obj-Direct2D1/
obj-Skia/

3
.gitmodules

@ -1,6 +1,3 @@
[submodule "src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github"]
path = src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github
url = https://github.com/AvaloniaUI/Portable.Xaml.git
[submodule "nukebuild/Numerge"]
path = nukebuild/Numerge
url = https://github.com/kekekeks/Numerge.git

1
Avalonia.sln

@ -146,7 +146,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1
build\Serilog.props = build\Serilog.props
build\SharpDX.props = build\SharpDX.props
build\SkiaSharp.props = build\SkiaSharp.props
build\Splat.props = build\Splat.props
build\System.Memory.props = build\System.Memory.props
build\XUnit.props = build\XUnit.props
EndProjectSection

5
dirs.proj

@ -8,10 +8,11 @@
<ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml/PortableXaml/**/*.*proj" />
<ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml/XamlIl/**/*.*proj" />
</ItemGroup>
<!-- Disabled on CI because of ancient MSBuild project format -->
<ItemGroup>
<ItemGroup Condition="!Exists('$(MSBuildExtensionsPath)\Xamarin\Android')">
<ProjectReference Remove="src/Android/**/*.*proj" />
<ProjectReference Remove="samples/ControlCatalog.Android/ControlCatalog.Android.csproj" />
</ItemGroup>
<ItemGroup Condition="!Exists('$(MSBuildExtensionsPath)\Xamarin\iOS') Or $([MSBuild]::IsOsPlatform('Windows')) ">
<ProjectReference Remove="src/iOS/**/*.*proj" />
<ProjectReference Remove="samples/ControlCatalog.iOS/ControlCatalog.iOS.csproj" />
</ItemGroup>

1
native/Avalonia.Native/inc/avalonia-native.h

@ -144,6 +144,7 @@ enum AvnStandardCursorType
CursorDragMove,
CursorDragCopy,
CursorDragLink,
CursorNone
};
enum AvnWindowEdge

10
native/Avalonia.Native/src/OSX/cursor.h

@ -11,18 +11,24 @@ class Cursor : public ComSingleObject<IAvnCursor, &IID_IAvnCursor>
{
private:
NSCursor * _native;
bool _isHidden;
public:
FORWARD_IUNKNOWN()
Cursor(NSCursor * cursor)
Cursor(NSCursor * cursor, bool isHidden = false)
{
_native = cursor;
_isHidden = isHidden;
}
NSCursor* GetNative()
{
return _native;
}
bool IsHidden ()
{
return _isHidden;
}
};
extern std::map<AvnStandardCursorType, Cursor*> s_cursorMap;

5
native/Avalonia.Native/src/OSX/cursor.mm

@ -21,6 +21,7 @@ class CursorFactory : public ComSingleObject<IAvnCursorFactory, &IID_IAvnCursorF
Cursor* resizeRightCursor = new Cursor([NSCursor resizeRightCursor]);
Cursor* resizeWestEastCursor = new Cursor([NSCursor resizeLeftRightCursor]);
Cursor* operationNotAllowedCursor = new Cursor([NSCursor operationNotAllowedCursor]);
Cursor* noCursor = new Cursor([NSCursor arrowCursor], true);
std::map<AvnStandardCursorType, Cursor*> s_cursorMap =
{
@ -46,11 +47,13 @@ class CursorFactory : public ComSingleObject<IAvnCursorFactory, &IID_IAvnCursorF
{ CursorIbeam, IBeamCursor },
{ CursorLeftSide, resizeLeftCursor },
{ CursorRightSide, resizeRightCursor },
{ CursorNo, operationNotAllowedCursor }
{ CursorNo, operationNotAllowedCursor },
{ CursorNone, noCursor }
};
public:
FORWARD_IUNKNOWN()
virtual HRESULT GetCursor (AvnStandardCursorType cursorType, IAvnCursor** retOut) override
{
*retOut = s_cursorMap[cursorType];

10
native/Avalonia.Native/src/OSX/window.mm

@ -353,6 +353,16 @@ public:
Cursor* avnCursor = dynamic_cast<Cursor*>(cursor);
this->cursor = avnCursor->GetNative();
UpdateCursor();
if(avnCursor->IsHidden())
{
[NSCursor hide];
}
else
{
[NSCursor unhide];
}
return S_OK;
}
}

1
packages/Avalonia/Avalonia.csproj

@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net461;netcoreapp2.0</TargetFrameworks>
<PackageId>Avalonia</PackageId>
</PropertyGroup>
<ItemGroup>

4
readme.md

@ -8,9 +8,9 @@
## About
Avalonia is a WPF-inspired cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of OSs: Windows (.NET Framework, .NET Core), Linux (GTK), MacOS, Android and iOS.
Avalonia is a WPF/UWP-inspired cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of OSs: Windows (.NET Framework, .NET Core), Linux (libX11), MacOS, Android (experimental) and iOS (exprerimental).
**Avalonia is currently in beta** which means that the framework is generally usable for writing applications, but there may be some bugs and breaking changes as we continue development.
**Avalonia is currently in beta** which means that the framework is generally usable for writing applications, but there may be some bugs and breaking changes as we continue development, for more details about the status see https://github.com/AvaloniaUI/Avalonia/issues/2239
| Control catalog | Desktop platforms | Mobile platforms |
|---|---|---|

1
samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj

@ -11,6 +11,7 @@
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Desktop\Avalonia.Desktop.csproj" />
<ProjectReference Include="..\..\src\Avalonia.X11\Avalonia.X11.csproj" />
<PackageReference Include="Avalonia.Angle.Windows.Natives" Version="2.1.0.2019013001"/>
</ItemGroup>

6
samples/ControlCatalog.NetCore/Program.cs

@ -46,6 +46,12 @@ namespace ControlCatalog.NetCore
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.With(new X11PlatformOptions {EnableMultiTouch = true})
.With(new Win32PlatformOptions
{
EnableMultitouch = true,
AllowEglInitialization = true
})
.UseSkia()
.UseReactiveUI();

1
samples/ControlCatalog/MainView.xaml

@ -31,6 +31,7 @@
<TabItem Header="Menu"><pages:MenuPage/></TabItem>
<TabItem Header="Notifications"><pages:NotificationsPage/></TabItem>
<TabItem Header="NumericUpDown"><pages:NumericUpDownPage/></TabItem>
<TabItem Header="Pointers (Touch)"><pages:PointersPage/></TabItem>
<TabItem Header="ProgressBar"><pages:ProgressBarPage/></TabItem>
<TabItem Header="RadioButton"><pages:RadioButtonPage/></TabItem>
<TabItem Header="Slider"><pages:SliderPage/></TabItem>

4
samples/ControlCatalog/Pages/DataGridPage.xaml

@ -11,7 +11,7 @@
<Setter Property="Background" Value="{Binding Path=GDP, Mode=OneWay, Converter={StaticResource GDPConverter}}" />
</Style>
</UserControl.Styles>
<Grid RowDefinitions="Auto,Auto">
<Grid RowDefinitions="Auto,*">
<StackPanel Orientation="Vertical" Spacing="4" Grid.Row="0">
<TextBlock Classes="h1">DataGrid</TextBlock>
<TextBlock Classes="h2">A control for displaying and interacting with a data source.</TextBlock>
@ -52,4 +52,4 @@
</TabItem>
</TabControl>
</Grid>
</UserControl>
</UserControl>

99
samples/ControlCatalog/Pages/PointersPage.cs

@ -0,0 +1,99 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Media.Immutable;
namespace ControlCatalog.Pages
{
public class PointersPage : Control
{
class PointerInfo
{
public Point Point { get; set; }
public Color Color { get; set; }
}
private static Color[] AllColors = new[]
{
Colors.Aqua,
Colors.Beige,
Colors.Chartreuse,
Colors.Coral,
Colors.Fuchsia,
Colors.Crimson,
Colors.Lavender,
Colors.Orange,
Colors.Orchid,
Colors.ForestGreen,
Colors.SteelBlue,
Colors.PapayaWhip,
Colors.PaleVioletRed,
Colors.Goldenrod,
Colors.Maroon,
Colors.Moccasin,
Colors.Navy,
Colors.Wheat,
Colors.Violet,
Colors.Sienna,
Colors.Indigo,
Colors.Honeydew
};
private Dictionary<IPointer, PointerInfo> _pointers = new Dictionary<IPointer, PointerInfo>();
public PointersPage()
{
ClipToBounds = true;
}
void UpdatePointer(PointerEventArgs e)
{
if (!_pointers.TryGetValue(e.Pointer, out var info))
{
if (e.RoutedEvent == PointerMovedEvent)
return;
var colors = AllColors.Except(_pointers.Values.Select(c => c.Color)).ToArray();
var color = colors[new Random().Next(0, colors.Length - 1)];
_pointers[e.Pointer] = info = new PointerInfo {Color = color};
}
info.Point = e.GetPosition(this);
InvalidateVisual();
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
UpdatePointer(e);
e.Pointer.Capture(this);
base.OnPointerPressed(e);
}
protected override void OnPointerMoved(PointerEventArgs e)
{
UpdatePointer(e);
base.OnPointerMoved(e);
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
_pointers.Remove(e.Pointer);
InvalidateVisual();
}
public override void Render(DrawingContext context)
{
context.FillRectangle(Brushes.Transparent, new Rect(default, Bounds.Size));
foreach (var pt in _pointers.Values)
{
var brush = new ImmutableSolidColorBrush(pt.Color);
context.DrawGeometry(brush, null, new EllipseGeometry(new Rect(pt.Point.X - 75, pt.Point.Y - 75,
150, 150)));
}
}
}
}

4
samples/ControlCatalog/Pages/TextBoxPage.xaml

@ -26,6 +26,10 @@
<TextBox Width="200" Text="Left aligned text" TextAlignment="Left" />
<TextBox Width="200" Text="Center aligned text" TextAlignment="Center" />
<TextBox Width="200" Text="Right aligned text" TextAlignment="Right" />
<TextBox Width="200" Text="Custom selection brush"
SelectionStart="5" SelectionEnd="22"
SelectionBrush="Green" SelectionForegroundBrush="Yellow"/>
<TextBox Width="200" Text="Custom caret brush" CaretBrush="DarkOrange"/>
</StackPanel>
<StackPanel Orientation="Vertical" Spacing="8">

3
samples/ControlCatalog/ViewModels/MenuPageViewModel.cs

@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Reactive;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using ReactiveUI;
@ -11,7 +12,7 @@ namespace ControlCatalog.ViewModels
public MenuPageViewModel()
{
OpenCommand = ReactiveCommand.CreateFromTask(Open);
SaveCommand = ReactiveCommand.Create(Save);
SaveCommand = ReactiveCommand.Create(Save, Observable.Return(false));
OpenRecentCommand = ReactiveCommand.Create<string>(OpenRecent);
MenuItems = new[]

3
samples/RenderDemo/MainWindow.xaml

@ -38,6 +38,9 @@
<TabItem Header="SkCanvas">
<pages:CustomSkiaPage/>
</TabItem>
<TabItem Header="RenderTargetBitmap">
<pages:RenderTargetBitmapPage/>
</TabItem>
</TabControl>
</DockPanel>
</Window>

49
samples/RenderDemo/Pages/RenderTargetBitmapPage.cs

@ -0,0 +1,49 @@
using System.Diagnostics;
using Avalonia;
using Avalonia.Controls;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using Avalonia.Visuals.Media.Imaging;
namespace RenderDemo.Pages
{
public class RenderTargetBitmapPage : Control
{
private RenderTargetBitmap _bitmap;
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
_bitmap = new RenderTargetBitmap(new PixelSize(200, 200), new Vector(96, 96));
base.OnAttachedToLogicalTree(e);
}
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
_bitmap.Dispose();
_bitmap = null;
base.OnDetachedFromLogicalTree(e);
}
readonly Stopwatch _st = Stopwatch.StartNew();
public override void Render(DrawingContext context)
{
using (var ctxi = _bitmap.CreateDrawingContext(null))
using(var ctx = new DrawingContext(ctxi, false))
using (ctx.PushPostTransform(Matrix.CreateTranslation(-100, -100)
* Matrix.CreateRotation(_st.Elapsed.TotalSeconds)
* Matrix.CreateTranslation(100, 100)))
{
ctxi.Clear(default);
ctx.FillRectangle(Brushes.Fuchsia, new Rect(50, 50, 100, 100));
}
context.DrawImage(_bitmap, 1,
new Rect(0, 0, 200, 200),
new Rect(0, 0, 200, 200));
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
base.Render(context);
}
}
}

16
src/Android/Avalonia.Android/Platform/Specific/Helpers/AndroidTouchEventsHelper.cs

@ -33,7 +33,7 @@ namespace Avalonia.Android.Platform.Specific.Helpers
return null;
}
RawMouseEventType? mouseEventType = null;
RawPointerEventType? mouseEventType = null;
var eventTime = DateTime.Now;
//Basic touch support
switch (e.Action)
@ -42,17 +42,17 @@ namespace Avalonia.Android.Platform.Specific.Helpers
//may be bot flood the evnt system with too many event especially on not so powerfull mobile devices
if ((eventTime - _lastTouchMoveEventTime).TotalMilliseconds > 10)
{
mouseEventType = RawMouseEventType.Move;
mouseEventType = RawPointerEventType.Move;
}
break;
case MotionEventActions.Down:
mouseEventType = RawMouseEventType.LeftButtonDown;
mouseEventType = RawPointerEventType.LeftButtonDown;
break;
case MotionEventActions.Up:
mouseEventType = RawMouseEventType.LeftButtonUp;
mouseEventType = RawPointerEventType.LeftButtonUp;
break;
}
@ -75,14 +75,14 @@ namespace Avalonia.Android.Platform.Specific.Helpers
//we need to generate mouse move before first mouse down event
//as this is the way buttons are working every time
//otherwise there is a problem sometimes
if (mouseEventType == RawMouseEventType.LeftButtonDown)
if (mouseEventType == RawPointerEventType.LeftButtonDown)
{
var me = new RawMouseEventArgs(mouseDevice, (uint)eventTime.Ticks, inputRoot,
RawMouseEventType.Move, _point, InputModifiers.None);
var me = new RawPointerEventArgs(mouseDevice, (uint)eventTime.Ticks, inputRoot,
RawPointerEventType.Move, _point, InputModifiers.None);
_view.Input(me);
}
var mouseEvent = new RawMouseEventArgs(mouseDevice, (uint)eventTime.Ticks, inputRoot,
var mouseEvent = new RawPointerEventArgs(mouseDevice, (uint)eventTime.Ticks, inputRoot,
mouseEventType.Value, _point, InputModifiers.LeftMouseButton);
_view.Input(mouseEvent);

119
src/Avalonia.Base/Utilities/MathUtilities.cs

@ -1,6 +1,9 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Runtime.InteropServices;
namespace Avalonia.Utilities
{
/// <summary>
@ -8,6 +11,89 @@ namespace Avalonia.Utilities
/// </summary>
public static class MathUtilities
{
/// <summary>
/// AreClose - Returns whether or not two doubles are "close". That is, whether or
/// not they are within epsilon of each other.
/// </summary>
/// <param name="value1"> The first double to compare. </param>
/// <param name="value2"> The second double to compare. </param>
public static bool AreClose(double value1, double value2)
{
//in case they are Infinities (then epsilon check does not work)
if (value1 == value2) return true;
double eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * double.Epsilon;
double delta = value1 - value2;
return (-eps < delta) && (eps > delta);
}
/// <summary>
/// LessThan - Returns whether or not the first double is less than the second double.
/// That is, whether or not the first is strictly less than *and* not within epsilon of
/// the other number.
/// </summary>
/// <param name="value1"> The first double to compare. </param>
/// <param name="value2"> The second double to compare. </param>
public static bool LessThan(double value1, double value2)
{
return (value1 < value2) && !AreClose(value1, value2);
}
/// <summary>
/// GreaterThan - Returns whether or not the first double is greater than the second double.
/// That is, whether or not the first is strictly greater than *and* not within epsilon of
/// the other number.
/// </summary>
/// <param name="value1"> The first double to compare. </param>
/// <param name="value2"> The second double to compare. </param>
public static bool GreaterThan(double value1, double value2)
{
return (value1 > value2) && !AreClose(value1, value2);
}
/// <summary>
/// LessThanOrClose - Returns whether or not the first double is less than or close to
/// the second double. That is, whether or not the first is strictly less than or within
/// epsilon of the other number.
/// </summary>
/// <param name="value1"> The first double to compare. </param>
/// <param name="value2"> The second double to compare. </param>
public static bool LessThanOrClose(double value1, double value2)
{
return (value1 < value2) || AreClose(value1, value2);
}
/// <summary>
/// GreaterThanOrClose - Returns whether or not the first double is greater than or close to
/// the second double. That is, whether or not the first is strictly greater than or within
/// epsilon of the other number.
/// </summary>
/// <param name="value1"> The first double to compare. </param>
/// <param name="value2"> The second double to compare. </param>
public static bool GreaterThanOrClose(double value1, double value2)
{
return (value1 > value2) || AreClose(value1, value2);
}
/// <summary>
/// IsOne - Returns whether or not the double is "close" to 1. Same as AreClose(double, 1),
/// but this is faster.
/// </summary>
/// <param name="value"> The double to compare to 1. </param>
public static bool IsOne(double value)
{
return Math.Abs(value - 1.0) < 10.0 * double.Epsilon;
}
/// <summary>
/// IsZero - Returns whether or not the double is "close" to 0. Same as AreClose(double, 0),
/// but this is faster.
/// </summary>
/// <param name="value"> The double to compare to 0. </param>
public static bool IsZero(double value)
{
return Math.Abs(value) < 10.0 * double.Epsilon;
}
/// <summary>
/// Clamps a value between a minimum and maximum value.
/// </summary>
@ -31,6 +117,39 @@ namespace Avalonia.Utilities
}
}
/// <summary>
/// Calculates the value to be used for layout rounding at high DPI.
/// </summary>
/// <param name="value">Input value to be rounded.</param>
/// <param name="dpiScale">Ratio of screen's DPI to layout DPI</param>
/// <returns>Adjusted value that will produce layout rounding on screen at high dpi.</returns>
/// <remarks>This is a layout helper method. It takes DPI into account and also does not return
/// the rounded value if it is unacceptable for layout, e.g. Infinity or NaN. It's a helper associated with
/// UseLayoutRounding property and should not be used as a general rounding utility.</remarks>
public static double RoundLayoutValue(double value, double dpiScale)
{
double newValue;
// If DPI == 1, don't use DPI-aware rounding.
if (!MathUtilities.AreClose(dpiScale, 1.0))
{
newValue = Math.Round(value * dpiScale) / dpiScale;
// If rounding produces a value unacceptable to layout (NaN, Infinity or MaxValue), use the original value.
if (double.IsNaN(newValue) ||
double.IsInfinity(newValue) ||
MathUtilities.AreClose(newValue, double.MaxValue))
{
newValue = value;
}
}
else
{
newValue = Math.Round(value);
}
return newValue;
}
/// <summary>
/// Clamps a value between a minimum and maximum value.
/// </summary>

11
src/Avalonia.Build.Tasks/Program.cs

@ -1,6 +1,7 @@
using System;
using System.Collections;
using System.IO;
using System.Linq;
using Microsoft.Build.Framework;
namespace Avalonia.Build.Tasks
@ -11,8 +12,14 @@ namespace Avalonia.Build.Tasks
{
if (args.Length != 3)
{
Console.Error.WriteLine("input references output");
return 1;
if (args.Length == 1)
args = new[] {"original.dll", "references", "out.dll"}
.Select(x => Path.Combine(args[0], x)).ToArray();
else
{
Console.Error.WriteLine("input references output");
return 1;
}
}
return new CompileAvaloniaXamlTask()

32
src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs

@ -4,6 +4,7 @@ using System.Linq;
using Avalonia.Utilities;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Collections.Generic;
using XamlIl.TypeSystem;
namespace Avalonia.Build.Tasks
@ -144,6 +145,37 @@ namespace Avalonia.Build.Tasks
});
}
private static bool MatchThisCall(Collection<Instruction> instructions, int idx)
{
var i = instructions[idx];
// A "normal" way of passing `this` to a static method:
// ldarg.0
// call void [Avalonia.Markup.Xaml]Avalonia.Markup.Xaml.AvaloniaXamlLoader::Load(object)
if (i.OpCode == OpCodes.Ldarg_0 || (i.OpCode == OpCodes.Ldarg && i.Operand?.Equals(0) == true))
return true;
/* F# way of using `this` in constructor emits a monstrosity like this:
IL_01c7: ldarg.0
IL_01c8: ldfld class [FSharp.Core]Microsoft.FSharp.Core.FSharpRef`1<class FVim.Cursor> FVim.Cursor::this
IL_01cd: call instance !0 class [FSharp.Core]Microsoft.FSharp.Core.FSharpRef`1<class FVim.Cursor>::get_contents()
IL_01d2: call !!0 [FSharp.Core]Microsoft.FSharp.Core.LanguagePrimitives/IntrinsicFunctions::CheckThis<class FVim.Cursor>(!!0)
IL_01d7: call void [Avalonia.Markup.Xaml]Avalonia.Markup.Xaml.AvaloniaXamlLoader::Load(object)
We check for the previous call to be Microsoft.FSharp.Core.LanguagePrimitives/IntrinsicFunctions::CheckThis
since it actually returns `this`
*/
if (i.OpCode == OpCodes.Call
&& i.Operand is GenericInstanceMethod gim
&& gim.Name == "CheckThis"
&& gim.DeclaringType.FullName == "Microsoft.FSharp.Core.LanguagePrimitives/IntrinsicFunctions")
return true;
return false;
}
}
}

13
src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs

@ -150,7 +150,8 @@ namespace Avalonia.Build.Tasks
classType = typeSystem.TargetAssembly.FindType(tn.Text);
if (classType == null)
throw new XamlIlParseException($"Unable to find type `{tn.Text}`", classDirective);
initialRoot.Type = new XamlIlAstClrTypeReference(classDirective, classType, false);
compiler.OverrideRootType(parsed,
new XamlIlAstClrTypeReference(classDirective, classType, false));
initialRoot.Children.Remove(classDirective);
}
@ -233,8 +234,7 @@ namespace Avalonia.Build.Tasks
var i = method.Body.Instructions;
for (var c = 1; c < i.Count; c++)
{
if (i[c - 1].OpCode == OpCodes.Ldarg_0
&& i[c].OpCode == OpCodes.Call)
if (i[c].OpCode == OpCodes.Call)
{
var op = i[c].Operand as MethodReference;
@ -253,8 +253,11 @@ namespace Avalonia.Build.Tasks
&& op.Parameters[0].ParameterType.FullName == "System.Object"
&& op.DeclaringType.FullName == "Avalonia.Markup.Xaml.AvaloniaXamlLoader")
{
i[c].Operand = trampoline;
foundXamlLoader = true;
if (MatchThisCall(i, c - 1))
{
i[c].Operand = trampoline;
foundXamlLoader = true;
}
}
}
}

1
src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj

@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<PackageId>Avalonia.Controls.DataGrid</PackageId>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />

25
src/Avalonia.Controls.DataGrid/DataGridRow.cs

@ -881,10 +881,10 @@ namespace Avalonia.Controls
&& (double.IsNaN(_detailsContent.Height))
&& (AreDetailsVisible)
&& (!double.IsNaN(_detailsDesiredHeight))
&& !DoubleUtil.AreClose(_detailsContent.Bounds.Height, _detailsDesiredHeight)
&& !DoubleUtil.AreClose(_detailsContent.Bounds.Inflate(_detailsContent.Margin).Height, _detailsDesiredHeight)
&& Slot != -1)
{
_detailsDesiredHeight = _detailsContent.Bounds.Height;
_detailsDesiredHeight = _detailsContent.Bounds.Inflate(_detailsContent.Margin).Height;
if (true)
{
@ -943,6 +943,16 @@ namespace Avalonia.Controls
_previousDetailsHeight = newValue.Height;
}
}
private void DetailsContent_BoundsChanged(Rect newValue)
{
if(_detailsContent != null)
DetailsContent_SizeChanged(newValue.Inflate(_detailsContent.Margin));
}
private void DetailsContent_MarginChanged(Thickness newValue)
{
if (_detailsContent != null)
DetailsContent_SizeChanged(_detailsContent.Bounds.Inflate(newValue));
}
//TODO Animation
// Sets AreDetailsVisible on the row and animates if necessary
@ -997,7 +1007,7 @@ namespace Avalonia.Controls
}
}
}
internal void ApplyDetailsTemplate(bool initializeDetailsPreferredHeight)
{
if (_detailsElement != null && AreDetailsVisible)
@ -1023,8 +1033,11 @@ namespace Avalonia.Controls
if (_detailsContent != null)
{
_detailsContentSizeSubscription =
_detailsContent.GetObservable(BoundsProperty)
.Subscribe(DetailsContent_SizeChanged);
System.Reactive.Disposables.StableCompositeDisposable.Create(
_detailsContent.GetObservable(BoundsProperty)
.Subscribe(DetailsContent_BoundsChanged),
_detailsContent.GetObservable(MarginProperty)
.Subscribe(DetailsContent_MarginChanged));
_detailsElement.Children.Add(_detailsContent);
}
}
@ -1053,4 +1066,4 @@ namespace Avalonia.Controls
//TODO Styles
}
}

15
src/Avalonia.Controls/Application.cs

@ -255,16 +255,13 @@ namespace Avalonia
if (MainWindow == null)
{
Dispatcher.UIThread.Post(() =>
if (!mainWindow.IsVisible)
{
if (!mainWindow.IsVisible)
{
mainWindow.Show();
}
mainWindow.Show();
}
MainWindow = mainWindow;
});
}
MainWindow = mainWindow;
}
return Run(new CancellationTokenSource());
}
@ -362,7 +359,7 @@ namespace Avalonia
}
/// <inheritdoc/>
bool IResourceProvider.TryGetResource(string key, out object value)
bool IResourceProvider.TryGetResource(object key, out object value)
{
value = null;
return (_resources?.TryGetResource(key, out value) ?? false) ||

30
src/Avalonia.Controls/Button.cs

@ -33,8 +33,6 @@ namespace Avalonia.Controls
/// </summary>
public class Button : ContentControl
{
private ICommand _command;
/// <summary>
/// Defines the <see cref="ClickMode"/> property.
/// </summary>
@ -75,6 +73,9 @@ namespace Avalonia.Controls
public static readonly StyledProperty<bool> IsPressedProperty =
AvaloniaProperty.Register<Button, bool>(nameof(IsPressed));
private ICommand _command;
private bool _commandCanExecute = true;
/// <summary>
/// Initializes static members of the <see cref="Button"/> class.
/// </summary>
@ -147,6 +148,8 @@ namespace Avalonia.Controls
private set { SetValue(IsPressedProperty, value); }
}
protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute;
/// <inheritdoc/>
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
@ -252,7 +255,6 @@ namespace Avalonia.Controls
if (e.MouseButton == MouseButton.Left)
{
e.Device.Capture(this);
IsPressed = true;
e.Handled = true;
@ -270,7 +272,6 @@ namespace Avalonia.Controls
if (IsPressed && e.MouseButton == MouseButton.Left)
{
e.Device.Capture(null);
IsPressed = false;
e.Handled = true;
@ -282,6 +283,11 @@ namespace Avalonia.Controls
}
}
protected override void OnPointerCaptureLost(PointerCaptureLostEventArgs e)
{
IsPressed = false;
}
protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status)
{
base.UpdateDataValidation(property, status);
@ -289,7 +295,11 @@ namespace Avalonia.Controls
{
if (status?.ErrorType == BindingErrorType.Error)
{
IsEnabled = false;
if (_commandCanExecute)
{
_commandCanExecute = false;
UpdateIsEffectivelyEnabled();
}
}
}
}
@ -348,9 +358,13 @@ namespace Avalonia.Controls
/// <param name="e">The event args.</param>
private void CanExecuteChanged(object sender, EventArgs e)
{
// HACK: Just set the IsEnabled property for the moment. This needs to be changed to
// use IsEnabledCore etc. but it will do for now.
IsEnabled = Command == null || Command.CanExecute(CommandParameter);
var canExecute = Command == null || Command.CanExecute(CommandParameter);
if (canExecute != _commandCanExecute)
{
_commandCanExecute = canExecute;
UpdateIsEffectivelyEnabled();
}
}
/// <summary>

13
src/Avalonia.Controls/Calendar/CalendarButton.cs

@ -176,18 +176,5 @@ namespace Avalonia.Controls.Primitives
if (e.MouseButton == MouseButton.Left)
CalendarLeftMouseButtonUp?.Invoke(this, e);
}
/// <summary>
/// We need to simulate the MouseLeftButtonUp event for the
/// CalendarButton that stays in Pressed state after MouseCapture is
/// released since there is no actual MouseLeftButtonUp event for the
/// release.
/// </summary>
/// <param name="e">Event arguments.</param>
internal void SendMouseLeftButtonUp(PointerReleasedEventArgs e)
{
e.Handled = false;
base.OnPointerReleased(e);
}
}
}

13
src/Avalonia.Controls/Calendar/CalendarDayButton.cs

@ -234,18 +234,5 @@ namespace Avalonia.Controls.Primitives
if (e.MouseButton == MouseButton.Left)
CalendarDayButtonMouseUp?.Invoke(this, e);
}
/// <summary>
/// We need to simulate the MouseLeftButtonUp event for the
/// CalendarDayButton that stays in Pressed state after MouseCapture is
/// released since there is no actual MouseLeftButtonUp event for the
/// release.
/// </summary>
/// <param name="e">Event arguments.</param>
internal void SendMouseLeftButtonUp(PointerReleasedEventArgs e)
{
e.Handled = false;
base.OnPointerReleased(e);
}
}
}

32
src/Avalonia.Controls/Calendar/CalendarItem.cs

@ -934,22 +934,6 @@ namespace Avalonia.Controls.Primitives
// The button is in Pressed state. Change the state to normal.
if (e.Device.Captured == b)
e.Device.Capture(null);
// null check is added for unit tests
if (_downEventArg != null)
{
var arg =
new PointerReleasedEventArgs()
{
Device = _downEventArg.Device,
MouseButton = _downEventArg.MouseButton,
Handled = _downEventArg.Handled,
InputModifiers = _downEventArg.InputModifiers,
Route = _downEventArg.Route,
Source = _downEventArg.Source
};
b.SendMouseLeftButtonUp(arg);
}
_lastCalendarDayButton = b;
}
}
@ -1221,21 +1205,7 @@ namespace Avalonia.Controls.Primitives
if (e.Device.Captured == b)
e.Device.Capture(null);
//b.ReleaseMouseCapture();
if (_downEventArgYearView != null)
{
var args =
new PointerReleasedEventArgs()
{
Device = _downEventArgYearView.Device,
MouseButton = _downEventArgYearView.MouseButton,
Handled = _downEventArgYearView.Handled,
InputModifiers = _downEventArgYearView.InputModifiers,
Route = _downEventArgYearView.Route,
Source = _downEventArgYearView.Source
};
b.SendMouseLeftButtonUp(args);
}
_lastCalendarButton = b;
}
}

10
src/Avalonia.Controls/ColumnDefinition.cs

@ -55,11 +55,7 @@ namespace Avalonia.Controls
/// <summary>
/// Gets the actual calculated width of the column.
/// </summary>
public double ActualWidth
{
get;
internal set;
}
public double ActualWidth => Parent?.GetFinalColumnDefinitionWidth(Index) ?? 0d;
/// <summary>
/// Gets or sets the maximum width of the column in DIPs.
@ -87,5 +83,9 @@ namespace Avalonia.Controls
get { return GetValue(WidthProperty); }
set { SetValue(WidthProperty, value); }
}
internal override GridLength UserSizeValueCache => this.Width;
internal override double UserMinSizeValueCache => this.MinWidth;
internal override double UserMaxSizeValueCache => this.MaxWidth;
}
}

7
src/Avalonia.Controls/ColumnDefinitions.cs

@ -1,6 +1,8 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Collections;
@ -9,14 +11,13 @@ namespace Avalonia.Controls
/// <summary>
/// A collection of <see cref="ColumnDefinition"/>s.
/// </summary>
public class ColumnDefinitions : AvaloniaList<ColumnDefinition>
public class ColumnDefinitions : DefinitionList<ColumnDefinition>
{
/// <summary>
/// Initializes a new instance of the <see cref="ColumnDefinitions"/> class.
/// </summary>
public ColumnDefinitions()
public ColumnDefinitions() : base ()
{
ResetBehavior = ResetBehavior.Remove;
}
/// <summary>

2
src/Avalonia.Controls/ComboBox.cs

@ -302,7 +302,7 @@ namespace Avalonia.Controls
}
}
private bool CanFocus(IControl control) => control.Focusable && control.IsEnabledCore && control.IsVisible;
private bool CanFocus(IControl control) => control.Focusable && control.IsEffectivelyEnabled && control.IsVisible;
private void UpdateSelectionBoxItem(object item)
{

732
src/Avalonia.Controls/DefinitionBase.cs

@ -1,26 +1,734 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
// This source file is adapted from the Windows Presentation Foundation project.
// (https://github.com/dotnet/wpf/)
//
// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using Avalonia;
using Avalonia.Collections;
using Avalonia.Utilities;
namespace Avalonia.Controls
{
/// <summary>
/// Base class for <see cref="ColumnDefinition"/> and <see cref="RowDefinition"/>.
/// DefinitionBase provides core functionality used internally by Grid
/// and ColumnDefinitionCollection / RowDefinitionCollection
/// </summary>
public class DefinitionBase : AvaloniaObject
public abstract class DefinitionBase : AvaloniaObject
{
/// <summary>
/// Defines the <see cref="SharedSizeGroup"/> property.
/// SharedSizeGroup property.
/// </summary>
public static readonly StyledProperty<string> SharedSizeGroupProperty =
AvaloniaProperty.Register<DefinitionBase, string>(nameof(SharedSizeGroup), inherits: true);
public string SharedSizeGroup
{
get { return (string)GetValue(SharedSizeGroupProperty); }
set { SetValue(SharedSizeGroupProperty, value); }
}
/// <summary>
/// Gets or sets the name of the shared size group of the column or row.
/// Callback to notify about entering model tree.
/// </summary>
public string SharedSizeGroup
internal void OnEnterParentTree()
{
get { return GetValue(SharedSizeGroupProperty); }
set { SetValue(SharedSizeGroupProperty, value); }
this.InheritanceParent = Parent;
if (_sharedState == null)
{
// start with getting SharedSizeGroup value.
// this property is NOT inhereted which should result in better overall perf.
string sharedSizeGroupId = SharedSizeGroup;
if (sharedSizeGroupId != null)
{
SharedSizeScope privateSharedSizeScope = PrivateSharedSizeScope;
if (privateSharedSizeScope != null)
{
_sharedState = privateSharedSizeScope.EnsureSharedState(sharedSizeGroupId);
_sharedState.AddMember(this);
}
}
}
}
/// <summary>
/// Callback to notify about exitting model tree.
/// </summary>
internal void OnExitParentTree()
{
_offset = 0;
if (_sharedState != null)
{
_sharedState.RemoveMember(this);
_sharedState = null;
}
}
/// <summary>
/// Performs action preparing definition to enter layout calculation mode.
/// </summary>
internal void OnBeforeLayout(Grid grid)
{
// reset layout state.
_minSize = 0;
LayoutWasUpdated = true;
// defer verification for shared definitions
if (_sharedState != null) { _sharedState.EnsureDeferredValidation(grid); }
}
/// <summary>
/// Updates min size.
/// </summary>
/// <param name="minSize">New size.</param>
internal void UpdateMinSize(double minSize)
{
_minSize = Math.Max(_minSize, minSize);
}
/// <summary>
/// Sets min size.
/// </summary>
/// <param name="minSize">New size.</param>
internal void SetMinSize(double minSize)
{
_minSize = minSize;
}
/// <remarks>
/// This method reflects Grid.SharedScopeProperty state by setting / clearing
/// dynamic property PrivateSharedSizeScopeProperty. Value of PrivateSharedSizeScopeProperty
/// is a collection of SharedSizeState objects for the scope.
/// </remarks>
internal static void OnIsSharedSizeScopePropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e)
{
if ((bool)e.NewValue)
{
SharedSizeScope sharedStatesCollection = new SharedSizeScope();
d.SetValue(PrivateSharedSizeScopeProperty, sharedStatesCollection);
}
else
{
d.ClearValue(PrivateSharedSizeScopeProperty);
}
}
/// <summary>
/// Returns <c>true</c> if this definition is a part of shared group.
/// </summary>
internal bool IsShared
{
get { return (_sharedState != null); }
}
/// <summary>
/// Internal accessor to user size field.
/// </summary>
internal GridLength UserSize
{
get { return (_sharedState != null ? _sharedState.UserSize : UserSizeValueCache); }
}
/// <summary>
/// Internal accessor to user min size field.
/// </summary>
internal double UserMinSize
{
get { return (UserMinSizeValueCache); }
}
/// <summary>
/// Internal accessor to user max size field.
/// </summary>
internal double UserMaxSize
{
get { return (UserMaxSizeValueCache); }
}
/// <summary>
/// DefinitionBase's index in the parents collection.
/// </summary>
internal int Index
{
get
{
return (_parentIndex);
}
set
{
Debug.Assert(value >= -1);
_parentIndex = value;
}
}
/// <summary>
/// Layout-time user size type.
/// </summary>
internal Grid.LayoutTimeSizeType SizeType
{
get { return (_sizeType); }
set { _sizeType = value; }
}
/// <summary>
/// Returns or sets measure size for the definition.
/// </summary>
internal double MeasureSize
{
get { return (_measureSize); }
set { _measureSize = value; }
}
/// <summary>
/// Returns definition's layout time type sensitive preferred size.
/// </summary>
/// <remarks>
/// Returned value is guaranteed to be true preferred size.
/// </remarks>
internal double PreferredSize
{
get
{
double preferredSize = MinSize;
if (_sizeType != Grid.LayoutTimeSizeType.Auto
&& preferredSize < _measureSize)
{
preferredSize = _measureSize;
}
return (preferredSize);
}
}
/// <summary>
/// Returns or sets size cache for the definition.
/// </summary>
internal double SizeCache
{
get { return (_sizeCache); }
set { _sizeCache = value; }
}
/// <summary>
/// Returns min size.
/// </summary>
internal double MinSize
{
get
{
double minSize = _minSize;
if (UseSharedMinimum
&& _sharedState != null
&& minSize < _sharedState.MinSize)
{
minSize = _sharedState.MinSize;
}
return (minSize);
}
}
/// <summary>
/// Returns min size, always taking into account shared state.
/// </summary>
internal double MinSizeForArrange
{
get
{
double minSize = _minSize;
if (_sharedState != null
&& (UseSharedMinimum || !LayoutWasUpdated)
&& minSize < _sharedState.MinSize)
{
minSize = _sharedState.MinSize;
}
return (minSize);
}
}
/// <summary>
/// Offset.
/// </summary>
internal double FinalOffset
{
get { return _offset; }
set { _offset = value; }
}
/// <summary>
/// Internal helper to access up-to-date UserSize property value.
/// </summary>
internal abstract GridLength UserSizeValueCache { get; }
/// <summary>
/// Internal helper to access up-to-date UserMinSize property value.
/// </summary>
internal abstract double UserMinSizeValueCache { get; }
/// <summary>
/// Internal helper to access up-to-date UserMaxSize property value.
/// </summary>
internal abstract double UserMaxSizeValueCache { get; }
internal Grid Parent { get; set; }
/// <summary>
/// SetFlags is used to set or unset one or multiple
/// flags on the object.
/// </summary>
private void SetFlags(bool value, Flags flags)
{
_flags = value ? (_flags | flags) : (_flags & (~flags));
}
/// <summary>
/// CheckFlagsAnd returns <c>true</c> if all the flags in the
/// given bitmask are set on the object.
/// </summary>
private bool CheckFlagsAnd(Flags flags)
{
return ((_flags & flags) == flags);
}
private static void OnSharedSizeGroupPropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e)
{
DefinitionBase definition = (DefinitionBase)d;
if (definition.Parent != null)
{
string sharedSizeGroupId = (string)e.NewValue;
if (definition._sharedState != null)
{
// if definition is already registered AND shared size group id is changing,
// then un-register the definition from the current shared size state object.
definition._sharedState.RemoveMember(definition);
definition._sharedState = null;
}
if ((definition._sharedState == null) && (sharedSizeGroupId != null))
{
SharedSizeScope privateSharedSizeScope = definition.PrivateSharedSizeScope;
if (privateSharedSizeScope != null)
{
// if definition is not registered and both: shared size group id AND private shared scope
// are available, then register definition.
definition._sharedState = privateSharedSizeScope.EnsureSharedState(sharedSizeGroupId);
definition._sharedState.AddMember(definition);
}
}
}
}
/// <remarks>
/// Verifies that Shared Size Group Property string
/// a) not empty.
/// b) contains only letters, digits and underscore ('_').
/// c) does not start with a digit.
/// </remarks>
private static string SharedSizeGroupPropertyValueValid(Control _, string value)
{
Contract.Requires<ArgumentNullException>(value != null);
string id = (string)value;
if (id != string.Empty)
{
int i = -1;
while (++i < id.Length)
{
bool isDigit = Char.IsDigit(id[i]);
if ((i == 0 && isDigit)
|| !(isDigit
|| Char.IsLetter(id[i])
|| '_' == id[i]))
{
break;
}
}
if (i == id.Length)
{
return value;
}
}
throw new ArgumentException("Invalid SharedSizeGroup string.");
}
/// <remark>
/// OnPrivateSharedSizeScopePropertyChanged is called when new scope enters or
/// existing scope just left. In both cases if the DefinitionBase object is already registered
/// in SharedSizeState, it should un-register and register itself in a new one.
/// </remark>
private static void OnPrivateSharedSizeScopePropertyChanged(AvaloniaObject d, AvaloniaPropertyChangedEventArgs e)
{
DefinitionBase definition = (DefinitionBase)d;
if (definition.Parent != null)
{
SharedSizeScope privateSharedSizeScope = (SharedSizeScope)e.NewValue;
if (definition._sharedState != null)
{
// if definition is already registered And shared size scope is changing,
// then un-register the definition from the current shared size state object.
definition._sharedState.RemoveMember(definition);
definition._sharedState = null;
}
if ((definition._sharedState == null) && (privateSharedSizeScope != null))
{
string sharedSizeGroup = definition.SharedSizeGroup;
if (sharedSizeGroup != null)
{
// if definition is not registered and both: shared size group id AND private shared scope
// are available, then register definition.
definition._sharedState = privateSharedSizeScope.EnsureSharedState(definition.SharedSizeGroup);
definition._sharedState.AddMember(definition);
}
}
}
}
/// <summary>
/// Private getter of shared state collection dynamic property.
/// </summary>
private SharedSizeScope PrivateSharedSizeScope
{
get { return (SharedSizeScope)GetValue(PrivateSharedSizeScopeProperty); }
}
/// <summary>
/// Convenience accessor to UseSharedMinimum flag
/// </summary>
private bool UseSharedMinimum
{
get { return (CheckFlagsAnd(Flags.UseSharedMinimum)); }
set { SetFlags(value, Flags.UseSharedMinimum); }
}
/// <summary>
/// Convenience accessor to LayoutWasUpdated flag
/// </summary>
private bool LayoutWasUpdated
{
get { return (CheckFlagsAnd(Flags.LayoutWasUpdated)); }
set { SetFlags(value, Flags.LayoutWasUpdated); }
}
private Flags _flags; // flags reflecting various aspects of internal state
internal int _parentIndex = -1; // this instance's index in parent's children collection
private Grid.LayoutTimeSizeType _sizeType; // layout-time user size type. it may differ from _userSizeValueCache.UnitType when calculating "to-content"
private double _minSize; // used during measure to accumulate size for "Auto" and "Star" DefinitionBase's
private double _measureSize; // size, calculated to be the input contstraint size for Child.Measure
private double _sizeCache; // cache used for various purposes (sorting, caching, etc) during calculations
private double _offset; // offset of the DefinitionBase from left / top corner (assuming LTR case)
private SharedSizeState _sharedState; // reference to shared state object this instance is registered with
[System.Flags]
private enum Flags : byte
{
//
// bool flags
//
UseSharedMinimum = 0x00000020, // when "1", definition will take into account shared state's minimum
LayoutWasUpdated = 0x00000040, // set to "1" every time the parent grid is measured
}
/// <summary>
/// Collection of shared states objects for a single scope
/// </summary>
internal class SharedSizeScope
{
/// <summary>
/// Returns SharedSizeState object for a given group.
/// Creates a new StatedState object if necessary.
/// </summary>
internal SharedSizeState EnsureSharedState(string sharedSizeGroup)
{
// check that sharedSizeGroup is not default
Debug.Assert(sharedSizeGroup != null);
SharedSizeState sharedState = _registry[sharedSizeGroup] as SharedSizeState;
if (sharedState == null)
{
sharedState = new SharedSizeState(this, sharedSizeGroup);
_registry[sharedSizeGroup] = sharedState;
}
return (sharedState);
}
/// <summary>
/// Removes an entry in the registry by the given key.
/// </summary>
internal void Remove(object key)
{
Debug.Assert(_registry.Contains(key));
_registry.Remove(key);
}
private Hashtable _registry = new Hashtable(); // storage for shared state objects
}
/// <summary>
/// Implementation of per shared group state object
/// </summary>
internal class SharedSizeState
{
/// <summary>
/// Default ctor.
/// </summary>
internal SharedSizeState(SharedSizeScope sharedSizeScope, string sharedSizeGroupId)
{
Debug.Assert(sharedSizeScope != null && sharedSizeGroupId != null);
_sharedSizeScope = sharedSizeScope;
_sharedSizeGroupId = sharedSizeGroupId;
_registry = new List<DefinitionBase>();
_layoutUpdated = new EventHandler(OnLayoutUpdated);
_broadcastInvalidation = true;
}
/// <summary>
/// Adds / registers a definition instance.
/// </summary>
internal void AddMember(DefinitionBase member)
{
Debug.Assert(!_registry.Contains(member));
_registry.Add(member);
Invalidate();
}
/// <summary>
/// Removes / un-registers a definition instance.
/// </summary>
/// <remarks>
/// If the collection of registered definitions becomes empty
/// instantiates self removal from owner's collection.
/// </remarks>
internal void RemoveMember(DefinitionBase member)
{
Invalidate();
_registry.Remove(member);
if (_registry.Count == 0)
{
_sharedSizeScope.Remove(_sharedSizeGroupId);
}
}
/// <summary>
/// Propogates invalidations for all registered definitions.
/// Resets its own state.
/// </summary>
internal void Invalidate()
{
_userSizeValid = false;
if (_broadcastInvalidation)
{
for (int i = 0, count = _registry.Count; i < count; ++i)
{
Grid parentGrid = (Grid)(_registry[i].Parent);
parentGrid.Invalidate();
}
_broadcastInvalidation = false;
}
}
/// <summary>
/// Makes sure that one and only one layout updated handler is registered for this shared state.
/// </summary>
internal void EnsureDeferredValidation(Control layoutUpdatedHost)
{
if (_layoutUpdatedHost == null)
{
_layoutUpdatedHost = layoutUpdatedHost;
_layoutUpdatedHost.LayoutUpdated += _layoutUpdated;
}
}
/// <summary>
/// DefinitionBase's specific code.
/// </summary>
internal double MinSize
{
get
{
if (!_userSizeValid) { EnsureUserSizeValid(); }
return (_minSize);
}
}
/// <summary>
/// DefinitionBase's specific code.
/// </summary>
internal GridLength UserSize
{
get
{
if (!_userSizeValid) { EnsureUserSizeValid(); }
return (_userSize);
}
}
private void EnsureUserSizeValid()
{
_userSize = new GridLength(1, GridUnitType.Auto);
for (int i = 0, count = _registry.Count; i < count; ++i)
{
Debug.Assert(_userSize.GridUnitType == GridUnitType.Auto
|| _userSize.GridUnitType == GridUnitType.Pixel);
GridLength currentGridLength = _registry[i].UserSizeValueCache;
if (currentGridLength.GridUnitType == GridUnitType.Pixel)
{
if (_userSize.GridUnitType == GridUnitType.Auto)
{
_userSize = currentGridLength;
}
else if (_userSize.Value < currentGridLength.Value)
{
_userSize = currentGridLength;
}
}
}
// taking maximum with user size effectively prevents squishy-ness.
// this is a "solution" to avoid shared definitions from been sized to
// different final size at arrange time, if / when different grids receive
// different final sizes.
_minSize = _userSize.IsAbsolute ? _userSize.Value : 0.0;
_userSizeValid = true;
}
/// <summary>
/// OnLayoutUpdated handler. Validates that all participating definitions
/// have updated min size value. Forces another layout update cycle if needed.
/// </summary>
private void OnLayoutUpdated(object sender, EventArgs e)
{
double sharedMinSize = 0;
// accumulate min size of all participating definitions
for (int i = 0, count = _registry.Count; i < count; ++i)
{
sharedMinSize = Math.Max(sharedMinSize, _registry[i].MinSize);
}
bool sharedMinSizeChanged = !MathUtilities.AreClose(_minSize, sharedMinSize);
// compare accumulated min size with min sizes of the individual definitions
for (int i = 0, count = _registry.Count; i < count; ++i)
{
DefinitionBase definitionBase = _registry[i];
if (sharedMinSizeChanged || definitionBase.LayoutWasUpdated)
{
// if definition's min size is different, then need to re-measure
if (!MathUtilities.AreClose(sharedMinSize, definitionBase.MinSize))
{
Grid parentGrid = (Grid)definitionBase.Parent;
parentGrid.InvalidateMeasure();
definitionBase.UseSharedMinimum = true;
}
else
{
definitionBase.UseSharedMinimum = false;
// if measure is valid then also need to check arrange.
// Note: definitionBase.SizeCache is volatile but at this point
// it contains up-to-date final size
if (!MathUtilities.AreClose(sharedMinSize, definitionBase.SizeCache))
{
Grid parentGrid = (Grid)definitionBase.Parent;
parentGrid.InvalidateArrange();
}
}
definitionBase.LayoutWasUpdated = false;
}
}
_minSize = sharedMinSize;
_layoutUpdatedHost.LayoutUpdated -= _layoutUpdated;
_layoutUpdatedHost = null;
_broadcastInvalidation = true;
}
// the scope this state belongs to
private readonly SharedSizeScope _sharedSizeScope;
// Id of the shared size group this object is servicing
private readonly string _sharedSizeGroupId;
// Registry of participating definitions
private readonly List<DefinitionBase> _registry;
// Instance event handler for layout updated event
private readonly EventHandler _layoutUpdated;
// Control for which layout updated event handler is registered
private Control _layoutUpdatedHost;
// "true" when broadcasting of invalidation is needed
private bool _broadcastInvalidation;
// "true" when _userSize is up to date
private bool _userSizeValid;
// shared state
private GridLength _userSize;
// shared state
private double _minSize;
}
/// <summary>
/// Private shared size scope property holds a collection of shared state objects for the a given shared size scope.
/// <see cref="OnIsSharedSizeScopePropertyChanged"/>
/// </summary>
internal static readonly AttachedProperty<SharedSizeScope> PrivateSharedSizeScopeProperty =
AvaloniaProperty.RegisterAttached<DefinitionBase, Control, SharedSizeScope>(
"PrivateSharedSizeScope",
defaultValue: null,
inherits: true);
/// <summary>
/// Shared size group property marks column / row definition as belonging to a group "Foo" or "Bar".
/// </summary>
/// <remarks>
/// Value of the Shared Size Group Property must satisfy the following rules:
/// <list type="bullet">
/// <item><description>
/// String must not be empty.
/// </description></item>
/// <item><description>
/// String must consist of letters, digits and underscore ('_') only.
/// </description></item>
/// <item><description>
/// String must not start with a digit.
/// </description></item>
/// </list>
/// </remarks>
public static readonly AttachedProperty<string> SharedSizeGroupProperty =
AvaloniaProperty.RegisterAttached<DefinitionBase, Control, string>(
"SharedSizeGroup",
validate: SharedSizeGroupPropertyValueValid);
/// <summary>
/// Static ctor. Used for static registration of properties.
/// </summary>
static DefinitionBase()
{
SharedSizeGroupProperty.Changed.AddClassHandler<DefinitionBase>(OnSharedSizeGroupPropertyChanged);
PrivateSharedSizeScopeProperty.Changed.AddClassHandler<DefinitionBase>(OnPrivateSharedSizeScopePropertyChanged);
}
}
}
}

60
src/Avalonia.Controls/DefinitionList.cs

@ -0,0 +1,60 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Collections;
namespace Avalonia.Controls
{
public abstract class DefinitionList<T> : AvaloniaList<T> where T : DefinitionBase
{
public DefinitionList()
{
ResetBehavior = ResetBehavior.Remove;
CollectionChanged += OnCollectionChanged;
}
internal bool IsDirty = true;
private Grid _parent;
internal Grid Parent
{
get => _parent;
set => SetParent(value);
}
private void SetParent(Grid value)
{
_parent = value;
foreach (var pair in this.Select((definitions, index) => (definitions, index)))
{
pair.definitions.Parent = value;
pair.definitions.Index = pair.index;
}
}
internal void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
foreach (var nI in this.Select((d, i) => (d, i)))
nI.d._parentIndex = nI.i;
foreach (var nD in e.NewItems?.Cast<DefinitionBase>()
?? Enumerable.Empty<DefinitionBase>())
{
nD.Parent = this.Parent;
nD.OnEnterParentTree();
}
foreach (var oD in e.OldItems?.Cast<DefinitionBase>()
?? Enumerable.Empty<DefinitionBase>())
{
oD.OnExitParentTree();
}
IsDirty = true;
}
}
}

3482
src/Avalonia.Controls/Grid.cs

File diff suppressed because it is too large

54
src/Avalonia.Controls/MenuItem.cs

@ -9,6 +9,7 @@ using Avalonia.Controls.Generators;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
@ -20,8 +21,6 @@ namespace Avalonia.Controls
/// </summary>
public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable
{
private ICommand _command;
/// <summary>
/// Defines the <see cref="Command"/> property.
/// </summary>
@ -91,9 +90,8 @@ namespace Avalonia.Controls
private static readonly ITemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => new StackPanel());
/// <summary>
/// The submenu popup.
/// </summary>
private ICommand _command;
private bool _commandCanExecute = true;
private Popup _popup;
/// <summary>
@ -231,6 +229,8 @@ namespace Avalonia.Controls
/// <inheritdoc/>
IMenuElement IMenuItem.Parent => Parent as IMenuElement;
protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute;
/// <inheritdoc/>
bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap);
@ -337,12 +337,9 @@ namespace Avalonia.Controls
{
base.OnPointerEnter(e);
RaiseEvent(new PointerEventArgs
{
Device = e.Device,
RoutedEvent = PointerEnterItemEvent,
Source = this,
});
var point = e.GetPointerPoint(null);
RaiseEvent(new PointerEventArgs(PointerEnterItemEvent, this, e.Pointer, this.VisualRoot, point.Position,
e.Timestamp, point.Properties, e.InputModifiers));
}
/// <inheritdoc/>
@ -350,12 +347,9 @@ namespace Avalonia.Controls
{
base.OnPointerLeave(e);
RaiseEvent(new PointerEventArgs
{
Device = e.Device,
RoutedEvent = PointerLeaveItemEvent,
Source = this,
});
var point = e.GetPointerPoint(null);
RaiseEvent(new PointerEventArgs(PointerLeaveItemEvent, this, e.Pointer, this.VisualRoot, point.Position,
e.Timestamp, point.Properties, e.InputModifiers));
}
/// <summary>
@ -400,6 +394,22 @@ namespace Avalonia.Controls
}
}
protected override void UpdateDataValidation(AvaloniaProperty property, BindingNotification status)
{
base.UpdateDataValidation(property, status);
if (property == CommandProperty)
{
if (status?.ErrorType == BindingErrorType.Error)
{
if (_commandCanExecute)
{
_commandCanExecute = false;
UpdateIsEffectivelyEnabled();
}
}
}
}
/// <summary>
/// Closes all submenus of the menu item.
/// </summary>
@ -443,9 +453,13 @@ namespace Avalonia.Controls
/// <param name="e">The event args.</param>
private void CanExecuteChanged(object sender, EventArgs e)
{
// HACK: Just set the IsEnabled property for the moment. This needs to be changed to
// use IsEnabledCore etc. but it will do for now.
IsEnabled = Command == null || Command.CanExecute(CommandParameter);
var canExecute = Command == null || Command.CanExecute(CommandParameter);
if (canExecute != _commandCanExecute)
{
_commandCanExecute = canExecute;
UpdateIsEffectivelyEnabled();
}
}
/// <summary>

4
src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs

@ -373,9 +373,9 @@ namespace Avalonia.Controls.Platform
protected internal virtual void RawInput(RawInputEventArgs e)
{
var mouse = e as RawMouseEventArgs;
var mouse = e as RawPointerEventArgs;
if (mouse?.Type == RawMouseEventType.NonClientLeftButtonDown)
if (mouse?.Type == RawPointerEventType.NonClientLeftButtonDown)
{
Menu.Close();
}

22
src/Avalonia.Controls/Platform/InProcessDragSource.cs

@ -43,7 +43,7 @@ namespace Avalonia.Platform
_lastPosition = default(Point);
_allowedEffects = allowedEffects;
using (_inputManager.PreProcess.OfType<RawMouseEventArgs>().Subscribe(ProcessMouseEvents))
using (_inputManager.PreProcess.OfType<RawPointerEventArgs>().Subscribe(ProcessMouseEvents))
{
using (_inputManager.PreProcess.OfType<RawKeyEventArgs>().Subscribe(ProcessKeyEvents))
{
@ -153,7 +153,7 @@ namespace Avalonia.Platform
}
}
private void ProcessMouseEvents(RawMouseEventArgs e)
private void ProcessMouseEvents(RawPointerEventArgs e)
{
if (!_initialInputModifiers.HasValue)
_initialInputModifiers = e.InputModifiers & MOUSE_INPUTMODIFIERS;
@ -174,22 +174,22 @@ namespace Avalonia.Platform
switch (e.Type)
{
case RawMouseEventType.LeftButtonDown:
case RawMouseEventType.RightButtonDown:
case RawMouseEventType.MiddleButtonDown:
case RawMouseEventType.NonClientLeftButtonDown:
case RawPointerEventType.LeftButtonDown:
case RawPointerEventType.RightButtonDown:
case RawPointerEventType.MiddleButtonDown:
case RawPointerEventType.NonClientLeftButtonDown:
CancelDragging();
e.Handled = true;
return;
case RawMouseEventType.LeaveWindow:
case RawPointerEventType.LeaveWindow:
RaiseEventAndUpdateCursor(RawDragEventType.DragLeave, e.Root, e.Position, e.InputModifiers); break;
case RawMouseEventType.LeftButtonUp:
case RawPointerEventType.LeftButtonUp:
CheckDraggingAccepted(InputModifiers.LeftMouseButton); break;
case RawMouseEventType.MiddleButtonUp:
case RawPointerEventType.MiddleButtonUp:
CheckDraggingAccepted(InputModifiers.MiddleMouseButton); break;
case RawMouseEventType.RightButtonUp:
case RawPointerEventType.RightButtonUp:
CheckDraggingAccepted(InputModifiers.RightMouseButton); break;
case RawMouseEventType.Move:
case RawPointerEventType.Move:
var mods = e.InputModifiers & MOUSE_INPUTMODIFIERS;
if (_initialInputModifiers.Value != mods)
{

69
src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
@ -64,6 +65,7 @@ namespace Avalonia.Controls.Presenters
private Vector _offset;
private IDisposable _logicalScrollSubscription;
private Size _viewport;
private Dictionary<int, Vector> _activeLogicalGestureScrolls;
/// <summary>
/// Initializes static members of the <see cref="ScrollContentPresenter"/> class.
@ -81,6 +83,7 @@ namespace Avalonia.Controls.Presenters
public ScrollContentPresenter()
{
AddHandler(RequestBringIntoViewEvent, BringIntoViewRequested);
AddHandler(Gestures.ScrollGestureEvent, OnScrollGesture);
this.GetObservable(ChildProperty).Subscribe(UpdateScrollableSubscription);
}
@ -227,6 +230,72 @@ namespace Avalonia.Controls.Presenters
return finalSize;
}
// Arbitrary chosen value, probably need to ask ILogicalScrollable
private const int LogicalScrollItemSize = 50;
private void OnScrollGesture(object sender, ScrollGestureEventArgs e)
{
if (Extent.Height > Viewport.Height || Extent.Width > Viewport.Width)
{
var scrollable = Child as ILogicalScrollable;
bool isLogical = scrollable?.IsLogicalScrollEnabled == true;
double x = Offset.X;
double y = Offset.Y;
Vector delta = default;
if (isLogical)
_activeLogicalGestureScrolls?.TryGetValue(e.Id, out delta);
delta += e.Delta;
if (Extent.Height > Viewport.Height)
{
double dy;
if (isLogical)
{
var logicalUnits = delta.Y / LogicalScrollItemSize;
delta = delta.WithY(delta.Y - logicalUnits * LogicalScrollItemSize);
dy = logicalUnits * scrollable.ScrollSize.Height;
}
else
dy = delta.Y;
y += dy;
y = Math.Max(y, 0);
y = Math.Min(y, Extent.Height - Viewport.Height);
}
if (Extent.Width > Viewport.Width)
{
double dx;
if (isLogical)
{
var logicalUnits = delta.X / LogicalScrollItemSize;
delta = delta.WithX(delta.X - logicalUnits * LogicalScrollItemSize);
dx = logicalUnits * scrollable.ScrollSize.Width;
}
else
dx = delta.X;
x += dx;
x = Math.Max(x, 0);
x = Math.Min(x, Extent.Width - Viewport.Width);
}
if (isLogical)
{
if (_activeLogicalGestureScrolls == null)
_activeLogicalGestureScrolls = new Dictionary<int, Vector>();
_activeLogicalGestureScrolls[e.Id] = delta;
}
Offset = new Vector(x, y);
e.Handled = true;
}
}
private void OnScrollGestureEnded(object sender, ScrollGestureEndedEventArgs e)
=> _activeLogicalGestureScrolls?.Remove(e.Id);
/// <inheritdoc/>
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
{

64
src/Avalonia.Controls/Presenters/TextPresenter.cs

@ -19,6 +19,15 @@ namespace Avalonia.Controls.Presenters
public static readonly StyledProperty<char> PasswordCharProperty =
AvaloniaProperty.Register<TextPresenter, char>(nameof(PasswordChar));
public static readonly StyledProperty<IBrush> SelectionBrushProperty =
AvaloniaProperty.Register<TextPresenter, IBrush>(nameof(SelectionBrushProperty));
public static readonly StyledProperty<IBrush> SelectionForegroundBrushProperty =
AvaloniaProperty.Register<TextPresenter, IBrush>(nameof(SelectionForegroundBrushProperty));
public static readonly StyledProperty<IBrush> CaretBrushProperty =
AvaloniaProperty.Register<TextPresenter, IBrush>(nameof(CaretBrushProperty));
public static readonly DirectProperty<TextPresenter, int> SelectionStartProperty =
TextBox.SelectionStartProperty.AddOwner<TextPresenter>(
o => o.SelectionStart,
@ -34,11 +43,12 @@ namespace Avalonia.Controls.Presenters
private int _selectionStart;
private int _selectionEnd;
private bool _caretBlink;
private IBrush _highlightBrush;
static TextPresenter()
{
AffectsRender<TextPresenter>(PasswordCharProperty);
AffectsRender<TextPresenter>(PasswordCharProperty,
SelectionBrushProperty, SelectionForegroundBrushProperty,
SelectionStartProperty, SelectionEndProperty);
}
public TextPresenter()
@ -79,6 +89,24 @@ namespace Avalonia.Controls.Presenters
set => SetValue(PasswordCharProperty, value);
}
public IBrush SelectionBrush
{
get => GetValue(SelectionBrushProperty);
set => SetValue(SelectionBrushProperty, value);
}
public IBrush SelectionForegroundBrush
{
get => GetValue(SelectionForegroundBrushProperty);
set => SetValue(SelectionForegroundBrushProperty, value);
}
public IBrush CaretBrush
{
get => GetValue(CaretBrushProperty);
set => SetValue(CaretBrushProperty, value);
}
public int SelectionStart
{
get
@ -129,14 +157,9 @@ namespace Avalonia.Controls.Presenters
var rects = FormattedText.HitTestTextRange(start, length);
if (_highlightBrush == null)
{
_highlightBrush = (IBrush)this.FindResource("HighlightBrush");
}
foreach (var rect in rects)
{
context.FillRectangle(_highlightBrush, rect);
context.FillRectangle(SelectionBrush, rect);
}
}
@ -144,16 +167,21 @@ namespace Avalonia.Controls.Presenters
if (selectionStart == selectionEnd)
{
var backgroundColor = (((Control)TemplatedParent).GetValue(BackgroundProperty) as SolidColorBrush)?.Color;
var caretBrush = Brushes.Black;
var caretBrush = CaretBrush;
if (backgroundColor.HasValue)
if (caretBrush is null)
{
byte red = (byte)~(backgroundColor.Value.R);
byte green = (byte)~(backgroundColor.Value.G);
byte blue = (byte)~(backgroundColor.Value.B);
caretBrush = new SolidColorBrush(Color.FromRgb(red, green, blue));
var backgroundColor = (((Control)TemplatedParent).GetValue(BackgroundProperty) as SolidColorBrush)?.Color;
if (backgroundColor.HasValue)
{
byte red = (byte)~(backgroundColor.Value.R);
byte green = (byte)~(backgroundColor.Value.G);
byte blue = (byte)~(backgroundColor.Value.B);
caretBrush = new SolidColorBrush(Color.FromRgb(red, green, blue));
}
else
caretBrush = Brushes.Black;
}
if (_caretBlink)
@ -252,7 +280,7 @@ namespace Avalonia.Controls.Presenters
{
result.Spans = new[]
{
new FormattedTextStyleSpan(start, length, foregroundBrush: Brushes.White),
new FormattedTextStyleSpan(start, length, SelectionForegroundBrush),
};
}

4
src/Avalonia.Controls/Primitives/Popup.cs

@ -421,9 +421,9 @@ namespace Avalonia.Controls.Primitives
private void ListenForNonClientClick(RawInputEventArgs e)
{
var mouse = e as RawMouseEventArgs;
var mouse = e as RawPointerEventArgs;
if (!StaysOpen && mouse?.Type == RawMouseEventType.NonClientLeftButtonDown)
if (!StaysOpen && mouse?.Type == RawPointerEventType.NonClientLeftButtonDown)
{
Close();
}

22
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -54,7 +54,8 @@ namespace Avalonia.Controls.Primitives
nameof(SelectedIndex),
o => o.SelectedIndex,
(o, v) => o.SelectedIndex = v,
unsetValue: -1);
unsetValue: -1,
defaultBindingMode: BindingMode.TwoWay);
/// <summary>
/// Defines the <see cref="SelectedItem"/> property.
@ -380,6 +381,7 @@ namespace Avalonia.Controls.Primitives
}
break;
case NotifyCollectionChangedAction.Move:
case NotifyCollectionChangedAction.Reset:
SelectedIndex = IndexOf(Items, SelectedItem);
break;
@ -644,20 +646,20 @@ namespace Avalonia.Controls.Primitives
/// <param name="desired">The desired items.</param>
internal static void SynchronizeItems(IList items, IEnumerable<object> desired)
{
int index = 0;
var index = 0;
foreach (var i in desired)
foreach (object item in desired)
{
if (index < items.Count)
int itemIndex = items.IndexOf(item);
if (itemIndex == -1)
{
if (items[index] != i)
{
items[index] = i;
}
items.Insert(index, item);
}
else
else if(itemIndex != index)
{
items.Add(i);
items.RemoveAt(itemIndex);
items.Insert(index, item);
}
++index;

2
src/Avalonia.Controls/Primitives/TemplatedControl.cs

@ -357,7 +357,7 @@ namespace Avalonia.Controls.Primitives
if (control.TemplatedParent == this)
{
foreach (IControl child in control.GetVisualChildren())
foreach (IControl child in control.GetLogicalChildren())
{
RegisterNames(child, nameScope);
}

18
src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs

@ -39,21 +39,21 @@ namespace Avalonia.Controls.Remote.Server
KeyboardDevice = AvaloniaLocator.Current.GetService<IKeyboardDevice>();
}
private static RawMouseEventType GetAvaloniaEventType (Avalonia.Remote.Protocol.Input.MouseButton button, bool pressed)
private static RawPointerEventType GetAvaloniaEventType (Avalonia.Remote.Protocol.Input.MouseButton button, bool pressed)
{
switch (button)
{
case Avalonia.Remote.Protocol.Input.MouseButton.Left:
return pressed ? RawMouseEventType.LeftButtonDown : RawMouseEventType.LeftButtonUp;
return pressed ? RawPointerEventType.LeftButtonDown : RawPointerEventType.LeftButtonUp;
case Avalonia.Remote.Protocol.Input.MouseButton.Middle:
return pressed ? RawMouseEventType.MiddleButtonDown : RawMouseEventType.MiddleButtonUp;
return pressed ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.MiddleButtonUp;
case Avalonia.Remote.Protocol.Input.MouseButton.Right:
return pressed ? RawMouseEventType.RightButtonDown : RawMouseEventType.RightButtonUp;
return pressed ? RawPointerEventType.RightButtonDown : RawPointerEventType.RightButtonUp;
default:
return RawMouseEventType.Move;
return RawPointerEventType.Move;
}
}
@ -166,11 +166,11 @@ namespace Avalonia.Controls.Remote.Server
{
Dispatcher.UIThread.Post(() =>
{
Input?.Invoke(new RawMouseEventArgs(
Input?.Invoke(new RawPointerEventArgs(
MouseDevice,
0,
InputRoot,
RawMouseEventType.Move,
RawPointerEventType.Move,
new Point(pointer.X, pointer.Y),
GetAvaloniaInputModifiers(pointer.Modifiers)));
}, DispatcherPriority.Input);
@ -179,7 +179,7 @@ namespace Avalonia.Controls.Remote.Server
{
Dispatcher.UIThread.Post(() =>
{
Input?.Invoke(new RawMouseEventArgs(
Input?.Invoke(new RawPointerEventArgs(
MouseDevice,
0,
InputRoot,
@ -192,7 +192,7 @@ namespace Avalonia.Controls.Remote.Server
{
Dispatcher.UIThread.Post(() =>
{
Input?.Invoke(new RawMouseEventArgs(
Input?.Invoke(new RawPointerEventArgs(
MouseDevice,
0,
InputRoot,

16
src/Avalonia.Controls/RowDefinition.cs

@ -29,7 +29,7 @@ namespace Avalonia.Controls
/// <summary>
/// Initializes a new instance of the <see cref="RowDefinition"/> class.
/// </summary>
public RowDefinition()
public RowDefinition()
{
}
@ -38,7 +38,7 @@ namespace Avalonia.Controls
/// </summary>
/// <param name="value">The height of the row.</param>
/// <param name="type">The height unit of the column.</param>
public RowDefinition(double value, GridUnitType type)
public RowDefinition(double value, GridUnitType type)
{
Height = new GridLength(value, type);
}
@ -47,7 +47,7 @@ namespace Avalonia.Controls
/// Initializes a new instance of the <see cref="RowDefinition"/> class.
/// </summary>
/// <param name="height">The height of the column.</param>
public RowDefinition(GridLength height)
public RowDefinition(GridLength height)
{
Height = height;
}
@ -55,11 +55,7 @@ namespace Avalonia.Controls
/// <summary>
/// Gets the actual calculated height of the row.
/// </summary>
public double ActualHeight
{
get;
internal set;
}
public double ActualHeight => Parent?.GetFinalRowDefinitionHeight(Index) ?? 0d;
/// <summary>
/// Gets or sets the maximum height of the row in DIPs.
@ -87,5 +83,9 @@ namespace Avalonia.Controls
get { return GetValue(HeightProperty); }
set { SetValue(HeightProperty, value); }
}
internal override GridLength UserSizeValueCache => this.Height;
internal override double UserMinSizeValueCache => this.MinHeight;
internal override double UserMaxSizeValueCache => this.MaxHeight;
}
}

5
src/Avalonia.Controls/RowDefinitions.cs

@ -9,14 +9,13 @@ namespace Avalonia.Controls
/// <summary>
/// A collection of <see cref="RowDefinition"/>s.
/// </summary>
public class RowDefinitions : AvaloniaList<RowDefinition>
public class RowDefinitions : DefinitionList<RowDefinition>
{
/// <summary>
/// Initializes a new instance of the <see cref="RowDefinitions"/> class.
/// </summary>
public RowDefinitions()
public RowDefinitions() : base()
{
ResetBehavior = ResetBehavior.Remove;
}
/// <summary>

18
src/Avalonia.Controls/TabControl.cs

@ -1,6 +1,7 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Linq;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Presenters;
@ -8,6 +9,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
@ -166,10 +168,24 @@ namespace Avalonia.Controls
{
base.OnPointerPressed(e);
if (e.MouseButton == MouseButton.Left)
if (e.MouseButton == MouseButton.Left && e.Pointer.Type == PointerType.Mouse)
{
e.Handled = UpdateSelectionFromEventSource(e.Source);
}
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
if (e.MouseButton == MouseButton.Left && e.Pointer.Type != PointerType.Mouse)
{
var container = GetContainerFromEventSource(e.Source);
if (container != null
&& container.GetVisualsAt(e.GetPosition(container))
.Any(c => container == c || container.IsVisualAncestorOf(c)))
{
e.Handled = UpdateSelectionFromEventSource(e.Source);
}
}
}
}
}

56
src/Avalonia.Controls/TextBox.cs

@ -38,6 +38,15 @@ namespace Avalonia.Controls
public static readonly StyledProperty<char> PasswordCharProperty =
AvaloniaProperty.Register<TextBox, char>(nameof(PasswordChar));
public static readonly StyledProperty<IBrush> SelectionBrushProperty =
AvaloniaProperty.Register<TextBox, IBrush>(nameof(SelectionBrushProperty));
public static readonly StyledProperty<IBrush> SelectionForegroundBrushProperty =
AvaloniaProperty.Register<TextBox, IBrush>(nameof(SelectionForegroundBrushProperty));
public static readonly StyledProperty<IBrush> CaretBrushProperty =
AvaloniaProperty.Register<TextBox, IBrush>(nameof(CaretBrushProperty));
public static readonly DirectProperty<TextBox, int> SelectionStartProperty =
AvaloniaProperty.RegisterDirect<TextBox, int>(
nameof(SelectionStart),
@ -169,6 +178,24 @@ namespace Avalonia.Controls
set => SetValue(PasswordCharProperty, value);
}
public IBrush SelectionBrush
{
get => GetValue(SelectionBrushProperty);
set => SetValue(SelectionBrushProperty, value);
}
public IBrush SelectionForegroundBrush
{
get => GetValue(SelectionForegroundBrushProperty);
set => SetValue(SelectionForegroundBrushProperty, value);
}
public IBrush CaretBrush
{
get => GetValue(CaretBrushProperty);
set => SetValue(CaretBrushProperty, value);
}
public int SelectionStart
{
get
@ -214,9 +241,9 @@ namespace Avalonia.Controls
if (!_ignoreTextChanges)
{
var caretIndex = CaretIndex;
SelectionStart = CoerceCaretIndex(SelectionStart, value?.Length ?? 0);
SelectionEnd = CoerceCaretIndex(SelectionEnd, value?.Length ?? 0);
CaretIndex = CoerceCaretIndex(caretIndex, value?.Length ?? 0);
SelectionStart = CoerceCaretIndex(SelectionStart, value);
SelectionEnd = CoerceCaretIndex(SelectionEnd, value);
CaretIndex = CoerceCaretIndex(caretIndex, value);
if (SetAndRaise(TextProperty, ref _text, value) && !_isUndoingRedoing)
{
@ -287,16 +314,11 @@ namespace Avalonia.Controls
{
DecideCaretVisibility();
}
e.Handled = true;
}
private void DecideCaretVisibility()
{
if (!IsReadOnly)
_presenter?.ShowCaret();
else
_presenter?.HideCaret();
_presenter.ShowCaret();
}
protected override void OnLostFocus(RoutedEventArgs e)
@ -456,7 +478,7 @@ namespace Avalonia.Controls
movement = true;
selection = false;
handled = true;
}
else if (Match(keymap.MoveCursorToTheEndOfLine))
{
@ -485,7 +507,7 @@ namespace Avalonia.Controls
movement = true;
selection = true;
handled = true;
}
else if (Match(keymap.MoveCursorToTheEndOfLineWithSelection))
{
@ -677,11 +699,15 @@ namespace Avalonia.Controls
}
}
private int CoerceCaretIndex(int value) => CoerceCaretIndex(value, Text?.Length ?? 0);
private int CoerceCaretIndex(int value) => CoerceCaretIndex(value, Text);
private int CoerceCaretIndex(int value, int length)
private int CoerceCaretIndex(int value, string text)
{
var text = Text;
if (text == null)
{
return 0;
}
var length = text.Length;
if (value < 0)
{
@ -691,7 +717,7 @@ namespace Avalonia.Controls
{
return length;
}
else if (value > 0 && text[value - 1] == '\r' && text[value] == '\n')
else if (value > 0 && text[value - 1] == '\r' && value < length && text[value] == '\n')
{
return value + 1;
}

705
src/Avalonia.Controls/Utils/GridLayout.cs

@ -1,705 +0,0 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using Avalonia.Layout;
using JetBrains.Annotations;
namespace Avalonia.Controls.Utils
{
/// <summary>
/// Contains algorithms that can help to measure and arrange a Grid.
/// </summary>
internal class GridLayout
{
/// <summary>
/// Initialize a new <see cref="GridLayout"/> instance from the column definitions.
/// The instance doesn't care about whether the definitions are rows or columns.
/// It will not calculate the column or row differently.
/// </summary>
internal GridLayout([NotNull] ColumnDefinitions columns)
{
if (columns == null) throw new ArgumentNullException(nameof(columns));
_conventions = columns.Count == 0
? new List<LengthConvention> { new LengthConvention() }
: columns.Select(x => new LengthConvention(x.Width, x.MinWidth, x.MaxWidth)).ToList();
}
/// <summary>
/// Initialize a new <see cref="GridLayout"/> instance from the row definitions.
/// The instance doesn't care about whether the definitions are rows or columns.
/// It will not calculate the column or row differently.
/// </summary>
internal GridLayout([NotNull] RowDefinitions rows)
{
if (rows == null) throw new ArgumentNullException(nameof(rows));
_conventions = rows.Count == 0
? new List<LengthConvention> { new LengthConvention() }
: rows.Select(x => new LengthConvention(x.Height, x.MinHeight, x.MaxHeight)).ToList();
}
/// <summary>
/// Gets the layout tolerance. If any length offset is less than this value, we will treat them the same.
/// </summary>
private const double LayoutTolerance = 1.0 / 256.0;
/// <summary>
/// Gets all the length conventions that come from column/row definitions.
/// These conventions provide cell limitations, such as the expected pixel length, the min/max pixel length and the * count.
/// </summary>
[NotNull]
private readonly List<LengthConvention> _conventions;
/// <summary>
/// Gets all the length conventions that come from the grid children.
/// </summary>
[NotNull]
private readonly List<AdditionalLengthConvention> _additionalConventions =
new List<AdditionalLengthConvention>();
/// <summary>
/// Appending these elements into the convention list helps lay them out according to their desired sizes.
/// <para/>
/// Some elements are not only in a single grid cell, they have one or more column/row spans,
/// and these elements may affect the grid layout especially the measuring procedure.<para/>
/// Append these elements into the convention list can help to layout them correctly through
/// their desired size. Only a small subset of children need to be measured before layout starts
/// and they will be called via the<paramref name="getDesiredLength"/> callback.
/// </summary>
/// <typeparam name="T">The grid children type.</typeparam>
/// <param name="source">
/// Contains the safe column/row index and its span.
/// Notice that we will not verify whether the range is in the column/row count,
/// so you should get the safe column/row info first.
/// </param>
/// <param name="getDesiredLength">
/// This callback will be called if the <see cref="GridLayout"/> thinks that a child should be
/// measured first. Usually, these are the children that have the * or Auto length.
/// </param>
internal void AppendMeasureConventions<T>([NotNull] IDictionary<T, (int index, int span)> source,
[NotNull] Func<T, double> getDesiredLength)
{
if (source == null) throw new ArgumentNullException(nameof(source));
if (getDesiredLength == null) throw new ArgumentNullException(nameof(getDesiredLength));
// M1/7. Find all the Auto and * length columns/rows. (M1/7 means the 1st procedure of measurement.)
// Only these columns/rows' layout can be affected by the child desired size.
//
// Find all columns/rows that have Auto or * length. We'll measure the children in advance.
// Only these kind of columns/rows will affect the Grid layout.
// Please note:
// - If the column / row has Auto length, the Grid.DesiredSize and the column width
// will be affected by the child's desired size.
// - If the column / row has* length, the Grid.DesiredSize will be affected by the
// child's desired size but the column width not.
// +-----------------------------------------------------------+
// | * | A | * | P | A | * | P | * | * |
// +-----------------------------------------------------------+
// _conventions: | min | max | | | min | | min max | max |
// _additionalC: |<- desired ->| |< desired >|
// _additionalC: |< desired >| |<- desired ->|
// 寻找所有行列范围中包含 Auto 和 * 的元素,使用全部可用尺寸提前测量。
// 因为只有这部分元素的布局才会被 Grid 的子元素尺寸影响。
// 请注意:
// - Auto 长度的行列必定会受到子元素布局影响,会影响到行列的布局长度和 Grid 本身的 DesiredSize;
// - 而对于 * 长度,只有 Grid.DesiredSize 会受到子元素布局影响,而行列长度不会受影响。
// Find all the Auto and * length columns/rows.
var found = new Dictionary<T, (int index, int span)>();
for (var i = 0; i < _conventions.Count; i++)
{
var index = i;
var convention = _conventions[index];
if (convention.Length.IsAuto || convention.Length.IsStar)
{
foreach (var pair in source.Where(x =>
x.Value.index <= index && index < x.Value.index + x.Value.span))
{
found[pair.Key] = pair.Value;
}
}
}
// Append these layout into the additional convention list.
foreach (var pair in found)
{
var t = pair.Key;
var (index, span) = pair.Value;
var desiredLength = getDesiredLength(t);
if (Math.Abs(desiredLength) > LayoutTolerance)
{
_additionalConventions.Add(new AdditionalLengthConvention(index, span, desiredLength));
}
}
}
/// <summary>
/// Run measure procedure according to the <paramref name="containerLength"/> and gets the <see cref="MeasureResult"/>.
/// </summary>
/// <param name="containerLength">
/// The container length. Usually, it is the constraint of the <see cref="Layoutable.MeasureOverride"/> method.
/// </param>
/// <param name="conventions">
/// Overriding conventions that allows the algorithm to handle external inputa
/// </param>
/// <returns>
/// The measured result that containing the desired size and all the column/row lengths.
/// </returns>
[NotNull, Pure]
internal MeasureResult Measure(double containerLength, IReadOnlyList<LengthConvention> conventions = null)
{
// Prepare all the variables that this method needs to use.
conventions = conventions ?? _conventions.Select(x => x.Clone()).ToList();
var starCount = conventions.Where(x => x.Length.IsStar).Sum(x => x.Length.Value);
var aggregatedLength = 0.0;
double starUnitLength;
// M2/7. Aggregate all the pixel lengths. Then we can get the remaining length by `containerLength - aggregatedLength`.
// We mark the aggregated length as "fix" because we can completely determine their values. Same as below.
//
// +-----------------------------------------------------------+
// | * | A | * | P | A | * | P | * | * |
// +-----------------------------------------------------------+
// |#fix#| |#fix#|
//
// 将全部的固定像素长度的行列长度累加。这样,containerLength - aggregatedLength 便能得到剩余长度。
// 我们会将所有能够确定下长度的行列标记为 fix。下同。
// 请注意:
// - 我们并没有直接从 containerLength 一直减下去,而是使用 aggregatedLength 进行累加,是因为无穷大相减得到的是 NaN,不利于后续计算。
aggregatedLength += conventions.Where(x => x.Length.IsAbsolute).Sum(x => x.Length.Value);
// M3/7. Fix all the * lengths that have reached the minimum.
//
// +-----------------------------------------------------------+
// | * | A | * | P | A | * | P | * | * |
// +-----------------------------------------------------------+
// | min | max | | | min | | min max | max |
// | fix | |#fix#| fix |
var shouldTestStarMin = true;
while (shouldTestStarMin)
{
// Calculate the unit * length to estimate the length of each column/row that has * length.
// Under this estimated length, check if there is a minimum value that has a length less than its constraint.
// If there is such a *, then fix the size of this cell, and then loop it again until there is no * that can be constrained by the minimum value.
//
// 计算单位 * 的长度,以便预估出每一个 * 行列的长度。
// 在此预估的长度下,从前往后寻找是否存在某个 * 长度已经小于其约束的最小值。
// 如果发现存在这样的 *,那么将此单元格的尺寸固定下来(Fix),然后循环重来,直至再也没有能被最小值约束的 *。
var @fixed = false;
starUnitLength = (containerLength - aggregatedLength) / starCount;
foreach (var convention in conventions.Where(x => x.Length.IsStar))
{
var (star, min) = (convention.Length.Value, convention.MinLength);
var starLength = star * starUnitLength;
if (starLength < min)
{
convention.Fix(min);
starLength = min;
aggregatedLength += starLength;
starCount -= star;
@fixed = true;
break;
}
}
shouldTestStarMin = @fixed;
}
// M4/7. Determine the absolute pixel size of all columns/rows that have an Auto length.
//
// +-----------------------------------------------------------+
// | * | A | * | P | A | * | P | * | * |
// +-----------------------------------------------------------+
// | min | max | | | min | | min max | max |
// |#fix#| | fix |#fix#| fix | fix |
var shouldTestAuto = true;
while (shouldTestAuto)
{
var @fixed = false;
starUnitLength = (containerLength - aggregatedLength) / starCount;
for (var i = 0; i < conventions.Count; i++)
{
var convention = conventions[i];
if (!convention.Length.IsAuto)
{
continue;
}
var more = ApplyAdditionalConventionsForAuto(conventions, i, starUnitLength);
convention.Fix(more);
aggregatedLength += more;
@fixed = true;
break;
}
shouldTestAuto = @fixed;
}
// M5/7. Expand the stars according to the additional conventions (usually the child desired length).
// We can't fix this kind of length, so we just mark them as desired (des).
//
// +-----------------------------------------------------------+
// | * | A | * | P | A | * | P | * | * |
// +-----------------------------------------------------------+
// | min | max | | | min | | min max | max |
// |#des#| fix |#des#| fix | fix | fix | fix | #des# |#des#|
var (minLengths, desiredStarMin) = AggregateAdditionalConventionsForStars(conventions);
aggregatedLength += desiredStarMin;
// M6/7. Determine the desired length of the grid for current container length. Its value is stored in desiredLength.
// Assume if the container has infinite length, the grid desired length is stored in greedyDesiredLength.
//
// +-----------------------------------------------------------+
// | * | A | * | P | A | * | P | * | * |
// +-----------------------------------------------------------+
// | min | max | | | min | | min max | max |
// |#des#| fix |#des#| fix | fix | fix | fix | #des# |#des#|
// Note: This table will be stored as the intermediate result into the MeasureResult and it will be reused by Arrange procedure.
//
// desiredLength = Math.Max(0.0, des + fix + des + fix + fix + fix + fix + des + des)
// greedyDesiredLength = des + fix + des + fix + fix + fix + fix + des + des
var desiredLength = containerLength - aggregatedLength >= 0.0 ? aggregatedLength : containerLength;
var greedyDesiredLength = aggregatedLength;
// M7/7. Expand all the rest stars. These stars have no conventions or only have
// max value they can be expanded from zero to constraint.
//
// +-----------------------------------------------------------+
// | * | A | * | P | A | * | P | * | * |
// +-----------------------------------------------------------+
// | min | max | | | min | | min max | max |
// |#fix#| fix |#fix#| fix | fix | fix | fix | #fix# |#fix#|
// Note: This table will be stored as the final result into the MeasureResult.
var dynamicConvention = ExpandStars(conventions, containerLength);
Clip(dynamicConvention, containerLength);
// Returns the measuring result.
return new MeasureResult(containerLength, desiredLength, greedyDesiredLength,
conventions, dynamicConvention, minLengths);
}
/// <summary>
/// Run arrange procedure according to the <paramref name="measure"/> and gets the <see cref="ArrangeResult"/>.
/// </summary>
/// <param name="finalLength">
/// The container length. Usually, it is the finalSize of the <see cref="Layoutable.ArrangeOverride"/> method.
/// </param>
/// <param name="measure">
/// The result that the measuring procedure returns. If it is null, a new measure procedure will run.
/// </param>
/// <returns>
/// The measured result that containing the desired size and all the column/row length.
/// </returns>
[NotNull, Pure]
public ArrangeResult Arrange(double finalLength, [CanBeNull] MeasureResult measure)
{
measure = measure ?? Measure(finalLength);
// If the arrange final length does not equal to the measure length, we should measure again.
if (finalLength - measure.ContainerLength > LayoutTolerance)
{
// If the final length is larger, we will rerun the whole measure.
measure = Measure(finalLength, measure.LeanLengthList);
}
else if (finalLength - measure.ContainerLength < -LayoutTolerance)
{
// If the final length is smaller, we measure the M6/6 procedure only.
var dynamicConvention = ExpandStars(measure.LeanLengthList, finalLength);
measure = new MeasureResult(finalLength, measure.DesiredLength, measure.GreedyDesiredLength,
measure.LeanLengthList, dynamicConvention, measure.MinLengths);
}
return new ArrangeResult(measure.LengthList);
}
/// <summary>
/// Use the <see cref="_additionalConventions"/> to calculate the fixed length of the Auto column/row.
/// </summary>
/// <param name="conventions">The convention list that all the * with minimum length are fixed.</param>
/// <param name="index">The column/row index that should be fixed.</param>
/// <param name="starUnitLength">The unit * length for the current rest length.</param>
/// <returns>The final length of the Auto length column/row.</returns>
[Pure]
private double ApplyAdditionalConventionsForAuto(IReadOnlyList<LengthConvention> conventions,
int index, double starUnitLength)
{
// 1. Calculate all the * length with starUnitLength.
// 2. Exclude all the fixed length and all the * length.
// 3. Compare the rest of the desired length and the convention.
// +-----------------+
// | * | A | * |
// +-----------------+
// | exl | | exl |
// |< desired >|
// |< desired >|
var more = 0.0;
foreach (var additional in _additionalConventions)
{
// If the additional convention's last column/row contains the Auto column/row, try to determine the Auto column/row length.
if (index == additional.Index + additional.Span - 1)
{
var min = Enumerable.Range(additional.Index, additional.Span)
.Select(x =>
{
var c = conventions[x];
if (c.Length.IsAbsolute) return c.Length.Value;
if (c.Length.IsStar) return c.Length.Value * starUnitLength;
return 0.0;
}).Sum();
more = Math.Max(additional.Min - min, more);
}
}
return Math.Min(conventions[index].MaxLength, more);
}
/// <summary>
/// Calculate the total desired length of all the * length.
/// Bug Warning:
/// - The behavior of this method is undefined! Different UI Frameworks have different behaviors.
/// - We ignore all the span columns/rows and just take single cells into consideration.
/// </summary>
/// <param name="conventions">All the conventions that have almost been fixed except the rest *.</param>
/// <returns>The total desired length of all the * length.</returns>
[Pure, MethodImpl(MethodImplOptions.AggressiveInlining)]
private (List<double>, double) AggregateAdditionalConventionsForStars(
IReadOnlyList<LengthConvention> conventions)
{
// 1. Determine all one-span column's desired widths or row's desired heights.
// 2. Order the multi-span conventions by its last index
// (Notice that the sorted data is much smaller than the source.)
// 3. Determine each multi-span last index by calculating the maximum desired size.
// Before we determine the behavior of this method, we just aggregate the one-span * columns.
var fixedLength = conventions.Where(x => x.Length.IsAbsolute).Sum(x => x.Length.Value);
// Prepare a lengthList variable indicating the fixed length of each column/row.
var lengthList = conventions.Select(x => x.Length.IsAbsolute ? x.Length.Value : 0.0).ToList();
foreach (var group in _additionalConventions
.Where(x => x.Span == 1 && conventions[x.Index].Length.IsStar)
.ToLookup(x => x.Index))
{
lengthList[group.Key] = Math.Max(lengthList[group.Key], group.Max(x => x.Min));
}
// Now the lengthList is fixed by every one-span columns/rows.
// Then we should determine the multi-span column's/row's length.
foreach (var group in _additionalConventions
.Where(x => x.Span > 1)
.ToLookup(x => x.Index + x.Span - 1)
// Order the multi-span columns/rows by last index.
.OrderBy(x => x.Key))
{
var length = group.Max(x => x.Min - Enumerable.Range(x.Index, x.Span - 1).Sum(r => lengthList[r]));
lengthList[group.Key] = Math.Max(lengthList[group.Key], length > 0 ? length : 0);
}
return (lengthList, lengthList.Sum() - fixedLength);
}
/// <summary>
/// This method implements the last procedure (M7/7) of measure.
/// It expands all the * length to the fixed length according to the <paramref name="constraint"/>.
/// </summary>
/// <param name="conventions">All the conventions that have almost been fixed except the remaining *.</param>
/// <param name="constraint">The container length.</param>
/// <returns>The final pixel length list.</returns>
[Pure]
private static List<double> ExpandStars(IEnumerable<LengthConvention> conventions, double constraint)
{
// Initial.
var dynamicConvention = conventions.Select(x => x.Clone()).ToList();
constraint -= dynamicConvention.Where(x => x.Length.IsAbsolute).Sum(x => x.Length.Value);
var starUnitLength = 0.0;
// M6/6.
if (constraint >= 0)
{
var starCount = dynamicConvention.Where(x => x.Length.IsStar).Sum(x => x.Length.Value);
var shouldTestStarMax = true;
while (shouldTestStarMax)
{
var @fixed = false;
starUnitLength = constraint / starCount;
foreach (var convention in dynamicConvention.Where(x =>
x.Length.IsStar && !double.IsPositiveInfinity(x.MaxLength)))
{
var (star, max) = (convention.Length.Value, convention.MaxLength);
var starLength = star * starUnitLength;
if (starLength > max)
{
convention.Fix(max);
starLength = max;
constraint -= starLength;
starCount -= star;
@fixed = true;
break;
}
}
shouldTestStarMax = @fixed;
}
}
Debug.Assert(dynamicConvention.All(x => !x.Length.IsAuto));
var starUnit = starUnitLength;
var result = dynamicConvention.Select(x =>
{
if (x.Length.IsStar)
{
return double.IsInfinity(starUnit) ? double.PositiveInfinity : starUnit * x.Length.Value;
}
return x.Length.Value;
}).ToList();
return result;
}
/// <summary>
/// If the container length is not infinity. It may be not enough to contain all the columns/rows.
/// We should clip the columns/rows that have been out of the container bounds.
/// Note: This method may change the items value of <paramref name="lengthList"/>.
/// </summary>
/// <param name="lengthList">A list of all the column widths and row heights with a fixed pixel length</param>
/// <param name="constraint">the container length. It can be positive infinity.</param>
private static void Clip([NotNull] IList<double> lengthList, double constraint)
{
if (double.IsInfinity(constraint))
{
return;
}
var measureLength = 0.0;
for (var i = 0; i < lengthList.Count; i++)
{
var length = lengthList[i];
if (constraint - measureLength > length)
{
measureLength += length;
}
else
{
lengthList[i] = constraint - measureLength;
measureLength = constraint;
}
}
}
/// <summary>
/// Contains the convention of each column/row.
/// This is mostly the same as <see cref="RowDefinition"/> or <see cref="ColumnDefinition"/>.
/// We use this because we can treat the column and the row the same.
/// </summary>
[DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
internal class LengthConvention : ICloneable
{
/// <summary>
/// Initialize a new instance of <see cref="LengthConvention"/>.
/// </summary>
public LengthConvention()
{
Length = new GridLength(1.0, GridUnitType.Star);
MinLength = 0.0;
MaxLength = double.PositiveInfinity;
}
/// <summary>
/// Initialize a new instance of <see cref="LengthConvention"/>.
/// </summary>
public LengthConvention(GridLength length, double minLength, double maxLength)
{
Length = length;
MinLength = minLength;
MaxLength = maxLength;
if (length.IsAbsolute)
{
_isFixed = true;
}
}
/// <summary>
/// Gets the <see cref="GridLength"/> of a column or a row.
/// </summary>
internal GridLength Length { get; private set; }
/// <summary>
/// Gets the minimum convention for a column or a row.
/// </summary>
internal double MinLength { get; }
/// <summary>
/// Gets the maximum convention for a column or a row.
/// </summary>
internal double MaxLength { get; }
/// <summary>
/// Fix the <see cref="LengthConvention"/>.
/// If all columns/rows are fixed, we can get the size of all columns/rows in pixels.
/// </summary>
/// <param name="pixel">
/// The pixel length that should be used to fix the convention.
/// </param>
/// <exception cref="InvalidOperationException">
/// If the convention is pixel length, this exception will throw.
/// </exception>
public void Fix(double pixel)
{
if (_isFixed)
{
throw new InvalidOperationException("Cannot fix the length convention if it is fixed.");
}
Length = new GridLength(pixel);
_isFixed = true;
}
/// <summary>
/// Gets a value that indicates whether this convention is fixed.
/// </summary>
private bool _isFixed;
/// <summary>
/// Helps the debugger to display the intermediate column/row calculation result.
/// </summary>
private string DebuggerDisplay =>
$"{(_isFixed ? Length.Value.ToString(CultureInfo.InvariantCulture) : (Length.GridUnitType == GridUnitType.Auto ? "Auto" : $"{Length.Value}*"))}, ∈[{MinLength}, {MaxLength}]";
/// <inheritdoc />
object ICloneable.Clone() => Clone();
/// <summary>
/// Get a deep copy of this convention list.
/// We need this because we want to store some intermediate states.
/// </summary>
internal LengthConvention Clone() => new LengthConvention(Length, MinLength, MaxLength);
}
/// <summary>
/// Contains the convention that comes from the grid children.
/// Some children span multiple columns or rows, so even a simple column/row can have multiple conventions.
/// </summary>
[DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")]
internal struct AdditionalLengthConvention
{
/// <summary>
/// Initialize a new instance of <see cref="AdditionalLengthConvention"/>.
/// </summary>
public AdditionalLengthConvention(int index, int span, double min)
{
Index = index;
Span = span;
Min = min;
}
/// <summary>
/// Gets the start index of this additional convention.
/// </summary>
public int Index { get; }
/// <summary>
/// Gets the span of this additional convention.
/// </summary>
public int Span { get; }
/// <summary>
/// Gets the minimum length of this additional convention.
/// This value is usually provided by the child's desired length.
/// </summary>
public double Min { get; }
/// <summary>
/// Helps the debugger to display the intermediate column/row calculation result.
/// </summary>
private string DebuggerDisplay =>
$"{{{string.Join(",", Enumerable.Range(Index, Span))}}}, ∈[{Min},∞)";
}
/// <summary>
/// Stores the result of the measuring procedure.
/// This result can be used to measure children and assign the desired size.
/// Passing this result to <see cref="Arrange"/> can reduce calculation.
/// </summary>
[DebuggerDisplay("{" + nameof(LengthList) + ",nq}")]
internal class MeasureResult
{
/// <summary>
/// Initialize a new instance of <see cref="MeasureResult"/>.
/// </summary>
internal MeasureResult(double containerLength, double desiredLength, double greedyDesiredLength,
IReadOnlyList<LengthConvention> leanConventions, IReadOnlyList<double> expandedConventions, IReadOnlyList<double> minLengths)
{
ContainerLength = containerLength;
DesiredLength = desiredLength;
GreedyDesiredLength = greedyDesiredLength;
LeanLengthList = leanConventions;
LengthList = expandedConventions;
MinLengths = minLengths;
}
/// <summary>
/// Gets the container length for this result.
/// This property will be used by <see cref="Arrange"/> to determine whether to measure again or not.
/// </summary>
public double ContainerLength { get; }
/// <summary>
/// Gets the desired length of this result.
/// Just return this value as the desired size in <see cref="Layoutable.MeasureOverride"/>.
/// </summary>
public double DesiredLength { get; }
/// <summary>
/// Gets the desired length if the container has infinite length.
/// </summary>
public double GreedyDesiredLength { get; }
/// <summary>
/// Contains the column/row calculation intermediate result.
/// This value is used by <see cref="Arrange"/> for reducing repeat calculation.
/// </summary>
public IReadOnlyList<LengthConvention> LeanLengthList { get; }
/// <summary>
/// Gets the length list for each column/row.
/// </summary>
public IReadOnlyList<double> LengthList { get; }
public IReadOnlyList<double> MinLengths { get; }
}
/// <summary>
/// Stores the result of the measuring procedure.
/// This result can be used to arrange children and assign the render size.
/// </summary>
[DebuggerDisplay("{" + nameof(LengthList) + ",nq}")]
internal class ArrangeResult
{
/// <summary>
/// Initialize a new instance of <see cref="ArrangeResult"/>.
/// </summary>
internal ArrangeResult(IReadOnlyList<double> lengthList)
{
LengthList = lengthList;
}
/// <summary>
/// Gets the length list for each column/row.
/// </summary>
public IReadOnlyList<double> LengthList { get; }
}
}
}

651
src/Avalonia.Controls/Utils/SharedSizeScopeHost.cs

@ -1,651 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Subjects;
using Avalonia.Collections;
using Avalonia.Controls.Utils;
using Avalonia.Layout;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
/// <summary>
/// Shared size scope implementation.
/// Shares the size information between participating grids.
/// An instance of this class is attached to every <see cref="Control"/> that has its
/// IsSharedSizeScope property set to true.
/// </summary>
internal sealed class SharedSizeScopeHost : IDisposable
{
private enum MeasurementState
{
Invalidated,
Measuring,
Cached
}
/// <summary>
/// Class containing the measured rows/columns for a single grid.
/// Monitors changes to the row/column collections as well as the SharedSizeGroup changes
/// for the individual items in those collections.
/// Notifies the <see cref="SharedSizeScopeHost"/> of SharedSizeGroup changes.
/// </summary>
private sealed class MeasurementCache : IDisposable
{
readonly CompositeDisposable _subscriptions;
readonly Subject<(string, string, MeasurementResult)> _groupChanged = new Subject<(string, string, MeasurementResult)>();
public ISubject<(string oldName, string newName, MeasurementResult result)> GroupChanged => _groupChanged;
public MeasurementCache(Grid grid)
{
Grid = grid;
Results = grid.RowDefinitions.Cast<DefinitionBase>()
.Concat(grid.ColumnDefinitions)
.Select(d => new MeasurementResult(grid, d))
.ToList();
grid.RowDefinitions.CollectionChanged += DefinitionsCollectionChanged;
grid.ColumnDefinitions.CollectionChanged += DefinitionsCollectionChanged;
_subscriptions = new CompositeDisposable(
Disposable.Create(() => grid.RowDefinitions.CollectionChanged -= DefinitionsCollectionChanged),
Disposable.Create(() => grid.ColumnDefinitions.CollectionChanged -= DefinitionsCollectionChanged),
grid.RowDefinitions.TrackItemPropertyChanged(DefinitionPropertyChanged),
grid.ColumnDefinitions.TrackItemPropertyChanged(DefinitionPropertyChanged));
}
// method to be hooked up once RowDefinitions/ColumnDefinitions collections can be replaced on a grid
private void DefinitionsChanged(object sender, AvaloniaPropertyChangedEventArgs e)
{
// route to collection changed as a Reset.
DefinitionsCollectionChanged(null, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
private void DefinitionPropertyChanged(Tuple<object, PropertyChangedEventArgs> propertyChanged)
{
if (propertyChanged.Item2.PropertyName == nameof(DefinitionBase.SharedSizeGroup))
{
var result = Results.Single(mr => ReferenceEquals(mr.Definition, propertyChanged.Item1));
var oldName = result.SizeGroup?.Name;
var newName = (propertyChanged.Item1 as DefinitionBase).SharedSizeGroup;
_groupChanged.OnNext((oldName, newName, result));
}
}
private void DefinitionsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
int offset = 0;
if (sender is ColumnDefinitions)
offset = Grid.RowDefinitions.Count;
var newItems = e.NewItems?.OfType<DefinitionBase>().Select(db => new MeasurementResult(Grid, db)).ToList() ?? new List<MeasurementResult>();
var oldItems = e.OldStartingIndex >= 0
? Results.GetRange(e.OldStartingIndex + offset, e.OldItems.Count)
: new List<MeasurementResult>();
void NotifyNewItems()
{
foreach (var item in newItems)
{
if (string.IsNullOrEmpty(item.Definition.SharedSizeGroup))
continue;
_groupChanged.OnNext((null, item.Definition.SharedSizeGroup, item));
}
}
void NotifyOldItems()
{
foreach (var item in oldItems)
{
if (string.IsNullOrEmpty(item.Definition.SharedSizeGroup))
continue;
_groupChanged.OnNext((item.Definition.SharedSizeGroup, null, item));
}
}
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
Results.InsertRange(e.NewStartingIndex + offset, newItems);
NotifyNewItems();
break;
case NotifyCollectionChangedAction.Remove:
Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count);
NotifyOldItems();
break;
case NotifyCollectionChangedAction.Move:
Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count);
Results.InsertRange(e.NewStartingIndex + offset, oldItems);
break;
case NotifyCollectionChangedAction.Replace:
Results.RemoveRange(e.OldStartingIndex + offset, oldItems.Count);
Results.InsertRange(e.NewStartingIndex + offset, newItems);
NotifyOldItems();
NotifyNewItems();
break;
case NotifyCollectionChangedAction.Reset:
oldItems = Results;
newItems = Results = Grid.RowDefinitions.Cast<DefinitionBase>()
.Concat(Grid.ColumnDefinitions)
.Select(d => new MeasurementResult(Grid, d))
.ToList();
NotifyOldItems();
NotifyNewItems();
break;
}
}
/// <summary>
/// Updates the Results collection with Grid Measure results.
/// </summary>
/// <param name="rowResult">Result of the GridLayout.Measure method for the RowDefinitions in the grid.</param>
/// <param name="columnResult">Result of the GridLayout.Measure method for the ColumnDefinitions in the grid.</param>
public void UpdateMeasureResult(GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult)
{
MeasurementState = MeasurementState.Cached;
for (int i = 0; i < Grid.RowDefinitions.Count; i++)
{
Results[i].MeasuredResult = rowResult.LengthList[i];
Results[i].MinLength = rowResult.MinLengths[i];
}
for (int i = 0; i < Grid.ColumnDefinitions.Count; i++)
{
Results[i + Grid.RowDefinitions.Count].MeasuredResult = columnResult.LengthList[i];
Results[i + Grid.RowDefinitions.Count].MinLength = columnResult.MinLengths[i];
}
}
/// <summary>
/// Clears the measurement cache, in preparation for the Measure pass.
/// </summary>
public void InvalidateMeasure()
{
var newItems = new List<MeasurementResult>();
var oldItems = new List<MeasurementResult>();
MeasurementState = MeasurementState.Invalidated;
Results.ForEach(r =>
{
r.MeasuredResult = double.NaN;
r.SizeGroup?.Reset();
});
}
/// <summary>
/// Clears the <see cref="IObservable{T}"/> subscriptions.
/// </summary>
public void Dispose()
{
_subscriptions.Dispose();
_groupChanged.OnCompleted();
}
/// <summary>
/// Gets the <see cref="Grid"/> for which this cache has been created.
/// </summary>
public Grid Grid { get; }
/// <summary>
/// Gets the <see cref="MeasurementState"/> of this cache.
/// </summary>
public MeasurementState MeasurementState { get; private set; }
/// <summary>
/// Gets the list of <see cref="MeasurementResult"/> instances.
/// </summary>
/// <remarks>
/// The list is a 1-1 map of the concatenation of RowDefinitions and ColumnDefinitions
/// </remarks>
public List<MeasurementResult> Results { get; private set; }
}
/// <summary>
/// Class containing the Measure result for a single Row/Column in a grid.
/// </summary>
private class MeasurementResult
{
public MeasurementResult(Grid owningGrid, DefinitionBase definition)
{
OwningGrid = owningGrid;
Definition = definition;
MeasuredResult = double.NaN;
}
/// <summary>
/// Gets the <see cref="RowDefinition"/>/<see cref="ColumnDefinition"/> related to this <see cref="MeasurementResult"/>
/// </summary>
public DefinitionBase Definition { get; }
/// <summary>
/// Gets or sets the actual result of the Measure operation for this column.
/// </summary>
public double MeasuredResult { get; set; }
/// <summary>
/// Gets or sets the Minimum constraint for a Row/Column - relevant for star Rows/Columns in unconstrained grids.
/// </summary>
public double MinLength { get; set; }
/// <summary>
/// Gets or sets the <see cref="Group"/> that this result belongs to.
/// </summary>
public Group SizeGroup { get; set; }
/// <summary>
/// Gets the Grid that is the parent of the Row/Column
/// </summary>
public Grid OwningGrid { get; }
/// <summary>
/// Calculates the effective length that this Row/Column wishes to enforce in the SharedSizeGroup.
/// </summary>
/// <returns>A tuple of length and the priority in the shared size group.</returns>
public (double length, int priority) GetPriorityLength()
{
var length = (Definition as ColumnDefinition)?.Width ?? ((RowDefinition)Definition).Height;
if (length.IsAbsolute)
return (MeasuredResult, 1);
if (length.IsAuto)
return (MeasuredResult, 2);
if (MinLength > 0)
return (MinLength, 3);
return (MeasuredResult, 4);
}
}
/// <summary>
/// Visitor class used to gather the final length for a given SharedSizeGroup.
/// </summary>
/// <remarks>
/// The values are applied according to priorities defined in <see cref="MeasurementResult.GetPriorityLength"/>.
/// </remarks>
private class LentgthGatherer
{
/// <summary>
/// Gets the final Length to be applied to every Row/Column in a SharedSizeGroup
/// </summary>
public double Length { get; private set; }
private int gatheredPriority = 6;
/// <summary>
/// Visits the <paramref name="result"/> applying the result of <see cref="MeasurementResult.GetPriorityLength"/> to its internal cache.
/// </summary>
/// <param name="result">The <see cref="MeasurementResult"/> instance to visit.</param>
public void Visit(MeasurementResult result)
{
var (length, priority) = result.GetPriorityLength();
if (gatheredPriority < priority)
return;
gatheredPriority = priority;
if (gatheredPriority == priority)
{
Length = Math.Max(length,Length);
}
else
{
Length = length;
}
}
}
/// <summary>
/// Representation of a SharedSizeGroup, containing Rows/Columns with the same SharedSizeGroup property value.
/// </summary>
private class Group
{
private double? cachedResult;
private List<MeasurementResult> _results = new List<MeasurementResult>();
/// <summary>
/// Gets the name of the SharedSizeGroup.
/// </summary>
public string Name { get; }
public Group(string name)
{
Name = name;
}
/// <summary>
/// Gets the collection of the <see cref="MeasurementResult"/> instances.
/// </summary>
public IReadOnlyList<MeasurementResult> Results => _results;
/// <summary>
/// Gets the final, calculated length for all Rows/Columns in the SharedSizeGroup.
/// </summary>
public double CalculatedLength => (cachedResult ?? (cachedResult = Gather())).Value;
/// <summary>
/// Clears the previously cached result in preparation for measurement.
/// </summary>
public void Reset()
{
cachedResult = null;
}
/// <summary>
/// Ads a measurement result to this group and sets it's <see cref="MeasurementResult.SizeGroup"/> property
/// to this instance.
/// </summary>
/// <param name="result">The <see cref="MeasurementResult"/> to include in this group.</param>
public void Add(MeasurementResult result)
{
if (_results.Contains(result))
throw new AvaloniaInternalException(
$"SharedSizeScopeHost: Invalid call to Group.Add - The SharedSizeGroup {Name} already contains the passed result");
result.SizeGroup = this;
_results.Add(result);
}
/// <summary>
/// Removes the measurement result from this group and clears its <see cref="MeasurementResult.SizeGroup"/> value.
/// </summary>
/// <param name="result">The <see cref="MeasurementResult"/> to clear.</param>
public void Remove(MeasurementResult result)
{
if (!_results.Contains(result))
throw new AvaloniaInternalException(
$"SharedSizeScopeHost: Invalid call to Group.Remove - The SharedSizeGroup {Name} does not contain the passed result");
result.SizeGroup = null;
_results.Remove(result);
}
private double Gather()
{
var visitor = new LentgthGatherer();
_results.ForEach(visitor.Visit);
return visitor.Length;
}
}
private readonly AvaloniaList<MeasurementCache> _measurementCaches = new AvaloniaList<MeasurementCache>();
private readonly Dictionary<string, Group> _groups = new Dictionary<string, Group>();
private bool _invalidating;
/// <summary>
/// Removes the SharedSizeScope and notifies all affected grids of the change.
/// </summary>
public void Dispose()
{
while (_measurementCaches.Any())
_measurementCaches[0].Grid.SharedScopeChanged();
}
/// <summary>
/// Registers the grid in this SharedSizeScope, to be called when the grid is added to the visual tree.
/// </summary>
/// <param name="toAdd">The <see cref="Grid"/> to add to this scope.</param>
internal void RegisterGrid(Grid toAdd)
{
if (_measurementCaches.Any(mc => ReferenceEquals(mc.Grid, toAdd)))
throw new AvaloniaInternalException("SharedSizeScopeHost: tried to register a grid twice!");
var cache = new MeasurementCache(toAdd);
_measurementCaches.Add(cache);
AddGridToScopes(cache);
}
/// <summary>
/// Removes the registration for a grid in this SharedSizeScope.
/// </summary>
/// <param name="toRemove">The <see cref="Grid"/> to remove.</param>
internal void UnegisterGrid(Grid toRemove)
{
var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, toRemove));
if (cache == null)
throw new AvaloniaInternalException("SharedSizeScopeHost: tried to unregister a grid that wasn't registered before!");
_measurementCaches.Remove(cache);
RemoveGridFromScopes(cache);
cache.Dispose();
}
/// <summary>
/// Helper method to check if a grid needs to forward its Mesure results to, and requrest Arrange results from this scope.
/// </summary>
/// <param name="toCheck">The <see cref="Grid"/> that should be checked.</param>
/// <returns>True if the grid should forward its calls.</returns>
internal bool ParticipatesInScope(Grid toCheck)
{
return _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, toCheck))
?.Results.Any(r => r.SizeGroup != null) ?? false;
}
/// <summary>
/// Notifies the SharedSizeScope that a grid had requested its measurement to be invalidated.
/// Forwards the same call to all affected grids in this scope.
/// </summary>
/// <param name="grid">The <see cref="Grid"/> that had it's Measure invalidated.</param>
internal void InvalidateMeasure(Grid grid)
{
// prevent stack overflow
if (_invalidating)
return;
_invalidating = true;
InvalidateMeasureImpl(grid);
_invalidating = false;
}
/// <summary>
/// Updates the measurement cache with the results of the <paramref name="grid"/> measurement pass.
/// </summary>
/// <param name="grid">The <see cref="Grid"/> that has been measured.</param>
/// <param name="rowResult">Measurement result for the grid's <see cref="RowDefinitions"/></param>
/// <param name="columnResult">Measurement result for the grid's <see cref="ColumnDefinitions"/></param>
internal void UpdateMeasureStatus(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult)
{
var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid));
if (cache == null)
throw new AvaloniaInternalException("SharedSizeScopeHost: Attempted to update measurement status for a grid that wasn't registered!");
cache.UpdateMeasureResult(rowResult, columnResult);
}
/// <summary>
/// Calculates the measurement result including the impact of any SharedSizeGroups that might affect this grid.
/// </summary>
/// <param name="grid">The <see cref="Grid"/> that is being Arranged</param>
/// <param name="rowResult">The <paramref name="grid"/>'s cached measurement result.</param>
/// <param name="columnResult">The <paramref name="grid"/>'s cached measurement result.</param>
/// <returns>Row and column measurement result updated with the SharedSizeScope constraints.</returns>
internal (GridLayout.MeasureResult, GridLayout.MeasureResult) HandleArrange(Grid grid, GridLayout.MeasureResult rowResult, GridLayout.MeasureResult columnResult)
{
return (
Arrange(grid.RowDefinitions, rowResult),
Arrange(grid.ColumnDefinitions, columnResult)
);
}
/// <summary>
/// Invalidates the measure of all grids affected by the SharedSizeGroups contained within.
/// </summary>
/// <param name="grid">The <see cref="Grid"/> that is being invalidated.</param>
private void InvalidateMeasureImpl(Grid grid)
{
var cache = _measurementCaches.FirstOrDefault(mc => ReferenceEquals(mc.Grid, grid));
if (cache == null)
throw new AvaloniaInternalException(
$"SharedSizeScopeHost: InvalidateMeasureImpl - called with a grid not present in the internal cache");
// already invalidated the cache, early out.
if (cache.MeasurementState == MeasurementState.Invalidated)
return;
// we won't calculate, so we should not invalidate.
if (!ParticipatesInScope(grid))
return;
cache.InvalidateMeasure();
// maybe there is a condition to only call arrange on some of the calls?
grid.InvalidateMeasure();
// find all the scopes within the invalidated grid
var scopeNames = cache.Results
.Where(mr => mr.SizeGroup != null)
.Select(mr => mr.SizeGroup.Name)
.Distinct();
// find all grids related to those scopes
var otherGrids = scopeNames.SelectMany(sn => _groups[sn].Results)
.Select(r => r.OwningGrid)
.Where(g => g.IsMeasureValid)
.Distinct();
// invalidate them as well
foreach (var otherGrid in otherGrids)
{
InvalidateMeasureImpl(otherGrid);
}
}
/// <summary>
/// <see cref="IObserver{T}"/> callback notifying the scope that a <see cref="MeasurementResult"/> has changed its
/// SharedSizeGroup
/// </summary>
/// <param name="change">Old and New name (either can be null) of the SharedSizeGroup, as well as the result.</param>
private void SharedGroupChanged((string oldName, string newName, MeasurementResult result) change)
{
RemoveFromGroup(change.oldName, change.result);
AddToGroup(change.newName, change.result);
}
/// <summary>
/// Handles the impact of SharedSizeGroups on the Arrange of <see cref="RowDefinitions"/>/<see cref="ColumnDefinitions"/>
/// </summary>
/// <param name="definitions">Rows/Columns that were measured</param>
/// <param name="measureResult">The initial measurement result.</param>
/// <returns>Modified measure result</returns>
private GridLayout.MeasureResult Arrange(IReadOnlyList<DefinitionBase> definitions, GridLayout.MeasureResult measureResult)
{
var conventions = measureResult.LeanLengthList.ToList();
var lengths = measureResult.LengthList.ToList();
var desiredLength = 0.0;
for (int i = 0; i < definitions.Count; i++)
{
var definition = definitions[i];
// for empty SharedSizeGroups pass on unmodified result.
if (string.IsNullOrEmpty(definition.SharedSizeGroup))
{
desiredLength += measureResult.LengthList[i];
continue;
}
var group = _groups[definition.SharedSizeGroup];
// Length calculated over all Definitions participating in a SharedSizeGroup.
var length = group.CalculatedLength;
conventions[i] = new GridLayout.LengthConvention(
new GridLength(length),
measureResult.LeanLengthList[i].MinLength,
measureResult.LeanLengthList[i].MaxLength
);
lengths[i] = length;
desiredLength += length;
}
return new GridLayout.MeasureResult(
measureResult.ContainerLength,
desiredLength,
measureResult.GreedyDesiredLength,//??
conventions,
lengths,
measureResult.MinLengths);
}
/// <summary>
/// Adds all measurement results for a grid to their repsective scopes.
/// </summary>
/// <param name="cache">The <see cref="MeasurementCache"/> for a grid to be added.</param>
private void AddGridToScopes(MeasurementCache cache)
{
cache.GroupChanged.Subscribe(SharedGroupChanged);
foreach (var result in cache.Results)
{
var scopeName = result.Definition.SharedSizeGroup;
AddToGroup(scopeName, result);
}
}
/// <summary>
/// Handles adding the <see cref="MeasurementResult"/> to a SharedSizeGroup.
/// Does nothing for empty SharedSizeGroups.
/// </summary>
/// <param name="scopeName">The name (can be null or empty) of the group to add the <paramref name="result"/> to.</param>
/// <param name="result">The <see cref="MeasurementResult"/> to add to a scope.</param>
private void AddToGroup(string scopeName, MeasurementResult result)
{
if (string.IsNullOrEmpty(scopeName))
return;
if (!_groups.TryGetValue(scopeName, out var group))
_groups.Add(scopeName, group = new Group(scopeName));
group.Add(result);
}
/// <summary>
/// Removes all measurement results for a grid from their respective scopes.
/// </summary>
/// <param name="cache">The <see cref="MeasurementCache"/> for a grid to be removed.</param>
private void RemoveGridFromScopes(MeasurementCache cache)
{
foreach (var result in cache.Results)
{
var scopeName = result.Definition.SharedSizeGroup;
RemoveFromGroup(scopeName, result);
}
}
/// <summary>
/// Handles removing the <see cref="MeasurementResult"/> from a SharedSizeGroup.
/// Does nothing for empty SharedSizeGroups.
/// </summary>
/// <param name="scopeName">The name (can be null or empty) of the group to remove the <paramref name="result"/> from.</param>
/// <param name="result">The <see cref="MeasurementResult"/> to remove from a scope.</param>
private void RemoveFromGroup(string scopeName, MeasurementResult result)
{
if (string.IsNullOrEmpty(scopeName))
return;
if (!_groups.TryGetValue(scopeName, out var group))
throw new AvaloniaInternalException($"SharedSizeScopeHost: The scope {scopeName} wasn't found in the shared size scope");
group.Remove(result);
if (!group.Results.Any())
_groups.Remove(scopeName);
}
}
}

14
src/Avalonia.Controls/Window.cs

@ -330,8 +330,7 @@ namespace Avalonia.Controls
protected virtual bool HandleClosing()
{
var args = new CancelEventArgs();
Closing?.Invoke(this, args);
OnClosing(args);
return args.Cancel;
}
@ -576,6 +575,17 @@ namespace Avalonia.Controls
base.HandleResized(clientSize);
}
/// <summary>
/// Raises the <see cref="Closing"/> event.
/// </summary>
/// <param name="e">The event args.</param>
/// <remarks>
/// A type that derives from <see cref="Window"/> may override <see cref="OnClosing"/>. The
/// overridden method must call <see cref="OnClosing"/> on the base class if the
/// <see cref="Closing"/> event needs to be raised.
/// </remarks>
protected virtual void OnClosing(CancelEventArgs e) => Closing?.Invoke(this, e);
}
}

196
src/Avalonia.Controls/WrapPanel.cs

@ -6,6 +6,7 @@ using System.Diagnostics;
using System.Linq;
using Avalonia.Input;
using Avalonia.Utilities;
using static System.Math;
@ -92,109 +93,127 @@ namespace Avalonia.Controls
}
}
private UVSize CreateUVSize(Size size) => new UVSize(Orientation, size);
private UVSize CreateUVSize() => new UVSize(Orientation);
/// <inheritdoc/>
protected override Size MeasureOverride(Size availableSize)
protected override Size MeasureOverride(Size constraint)
{
var desiredSize = CreateUVSize();
var lineSize = CreateUVSize();
var uvAvailableSize = CreateUVSize(availableSize);
var curLineSize = new UVSize(Orientation);
var panelSize = new UVSize(Orientation);
var uvConstraint = new UVSize(Orientation, constraint.Width, constraint.Height);
foreach (var child in Children)
var childConstraint = new Size(constraint.Width, constraint.Height);
for (int i = 0, count = Children.Count; i < count; i++)
{
child.Measure(availableSize);
var childSize = CreateUVSize(child.DesiredSize);
if (lineSize.U + childSize.U <= uvAvailableSize.U) // same line
var child = Children[i];
if (child == null) continue;
//Flow passes its own constrint to children
child.Measure(childConstraint);
//this is the size of the child in UV space
var sz = new UVSize(Orientation, child.DesiredSize.Width, child.DesiredSize.Height);
if (MathUtilities.GreaterThan(curLineSize.U + sz.U, uvConstraint.U)) //need to switch to another line
{
lineSize.U += childSize.U;
lineSize.V = Max(lineSize.V, childSize.V);
panelSize.U = Max(curLineSize.U, panelSize.U);
panelSize.V += curLineSize.V;
curLineSize = sz;
if (MathUtilities.GreaterThan(sz.U, uvConstraint.U)) //the element is wider then the constrint - give it a separate line
{
panelSize.U = Max(sz.U, panelSize.U);
panelSize.V += sz.V;
curLineSize = new UVSize(Orientation);
}
}
else // moving to next line
else //continue to accumulate a line
{
desiredSize.U = Max(lineSize.U, uvAvailableSize.U);
desiredSize.V += lineSize.V;
lineSize = childSize;
curLineSize.U += sz.U;
curLineSize.V = Max(sz.V, curLineSize.V);
}
}
// last element
desiredSize.U = Max(lineSize.U, desiredSize.U);
desiredSize.V += lineSize.V;
return desiredSize.ToSize();
//the last line size, if any should be added
panelSize.U = Max(curLineSize.U, panelSize.U);
panelSize.V += curLineSize.V;
//go from UV space to W/H space
return new Size(panelSize.Width, panelSize.Height);
}
/// <inheritdoc/>
protected override Size ArrangeOverride(Size finalSize)
{
int firstInLine = 0;
double accumulatedV = 0;
var uvFinalSize = CreateUVSize(finalSize);
var lineSize = CreateUVSize();
int firstChildInLineIndex = 0;
for (int index = 0; index < Children.Count; index++)
UVSize curLineSize = new UVSize(Orientation);
UVSize uvFinalSize = new UVSize(Orientation, finalSize.Width, finalSize.Height);
for (int i = 0; i < Children.Count; i++)
{
var child = Children[index];
var childSize = CreateUVSize(child.DesiredSize);
if (lineSize.U + childSize.U <= uvFinalSize.U) // same line
var child = Children[i];
if (child == null) continue;
var sz = new UVSize(Orientation, child.DesiredSize.Width, child.DesiredSize.Height);
if (MathUtilities.GreaterThan(curLineSize.U + sz.U, uvFinalSize.U)) //need to switch to another line
{
lineSize.U += childSize.U;
lineSize.V = Max(lineSize.V, childSize.V);
arrangeLine(accumulatedV, curLineSize.V, firstInLine, i);
accumulatedV += curLineSize.V;
curLineSize = sz;
if (MathUtilities.GreaterThan(sz.U, uvFinalSize.U)) //the element is wider then the constraint - give it a separate line
{
//switch to next line which only contain one element
arrangeLine(accumulatedV, sz.V, i, ++i);
accumulatedV += sz.V;
curLineSize = new UVSize(Orientation);
}
firstInLine = i;
}
else // moving to next line
else //continue to accumulate a line
{
var controlsInLine = GetControlsBetween(firstChildInLineIndex, index);
ArrangeLine(accumulatedV, lineSize.V, controlsInLine);
accumulatedV += lineSize.V;
lineSize = childSize;
firstChildInLineIndex = index;
curLineSize.U += sz.U;
curLineSize.V = Max(sz.V, curLineSize.V);
}
}
if (firstChildInLineIndex < Children.Count)
//arrange the last line, if any
if (firstInLine < Children.Count)
{
var controlsInLine = GetControlsBetween(firstChildInLineIndex, Children.Count);
ArrangeLine(accumulatedV, lineSize.V, controlsInLine);
arrangeLine(accumulatedV, curLineSize.V, firstInLine, Children.Count);
}
return finalSize;
}
private IEnumerable<IControl> GetControlsBetween(int first, int last)
{
return Children.Skip(first).Take(last - first);
return finalSize;
}
private void ArrangeLine(double v, double lineV, IEnumerable<IControl> controls)
private void arrangeLine(double v, double lineV, int start, int end)
{
double u = 0;
bool isHorizontal = (Orientation == Orientation.Horizontal);
foreach (var child in controls)
for (int i = start; i < end; i++)
{
var childSize = CreateUVSize(child.DesiredSize);
var x = isHorizontal ? u : v;
var y = isHorizontal ? v : u;
var width = isHorizontal ? childSize.U : lineV;
var height = isHorizontal ? lineV : childSize.U;
child.Arrange(new Rect(x, y, width, height));
u += childSize.U;
var child = Children[i];
if (child != null)
{
UVSize childSize = new UVSize(Orientation, child.DesiredSize.Width, child.DesiredSize.Height);
double layoutSlotU = childSize.U;
child.Arrange(new Rect(
(isHorizontal ? u : v),
(isHorizontal ? v : u),
(isHorizontal ? layoutSlotU : lineV),
(isHorizontal ? lineV : layoutSlotU)));
u += layoutSlotU;
}
}
}
/// <summary>
/// Used to not not write separate code for horizontal and vertical orientation.
/// U is direction in line. (x if orientation is horizontal)
/// V is direction of lines. (y if orientation is horizontal)
/// </summary>
[DebuggerDisplay("U = {U} V = {V}")]
private struct UVSize
{
private readonly Orientation _orientation;
internal double U;
internal double V;
private UVSize(Orientation orientation, double width, double height)
internal UVSize(Orientation orientation, double width, double height)
{
U = V = 0d;
_orientation = orientation;
@ -202,52 +221,25 @@ namespace Avalonia.Controls
Height = height;
}
internal UVSize(Orientation orientation, Size size)
: this(orientation, size.Width, size.Height)
{
}
internal UVSize(Orientation orientation)
{
U = V = 0d;
_orientation = orientation;
}
private double Width
internal double U;
internal double V;
private Orientation _orientation;
internal double Width
{
get { return (_orientation == Orientation.Horizontal ? U : V); }
set
{
if (_orientation == Orientation.Horizontal)
{
U = value;
}
else
{
V = value;
}
}
set { if (_orientation == Orientation.Horizontal) U = value; else V = value; }
}
private double Height
internal double Height
{
get { return (_orientation == Orientation.Horizontal ? V : U); }
set
{
if (_orientation == Orientation.Horizontal)
{
V = value;
}
else
{
U = value;
}
}
}
public Size ToSize()
{
return new Size(Width, Height);
set { if (_orientation == Orientation.Horizontal) V = value; else U = value; }
}
}
}

6
src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs

@ -9,7 +9,6 @@ using Avalonia.Remote.Protocol;
using Avalonia.Remote.Protocol.Designer;
using Avalonia.Remote.Protocol.Viewport;
using Avalonia.Threading;
using Portable.Xaml;
namespace Avalonia.DesignerSupport.Remote
{
@ -206,7 +205,6 @@ namespace Avalonia.DesignerSupport.Remote
}
catch (Exception e)
{
var xamlException = e as XamlException;
var xmlException = e as XmlException;
s_transport.Send(new UpdateXamlResultMessage
@ -216,8 +214,8 @@ namespace Avalonia.DesignerSupport.Remote
{
ExceptionType = e.GetType().FullName,
Message = e.Message.ToString(),
LineNumber = xamlException?.LineNumber ?? xmlException?.LineNumber,
LinePosition = xamlException?.LinePosition ?? xmlException?.LinePosition,
LineNumber = xmlException?.LineNumber,
LinePosition = xmlException?.LinePosition,
}
});
}

1
src/Avalonia.Desktop/Avalonia.Desktop.csproj

@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<PackageId>Avalonia.Desktop</PackageId>
</PropertyGroup>
<ItemGroup>

1
src/Avalonia.Input/Cursors.cs

@ -38,6 +38,7 @@ namespace Avalonia.Input
DragMove,
DragCopy,
DragLink,
None,
// Not available in GTK directly, see http://www.pixelbeat.org/programming/x_cursors/
// We might enable them later, preferably, by loading pixmax direclty from theme with fallback image

2
src/Avalonia.Input/FocusManager.cs

@ -146,7 +146,7 @@ namespace Avalonia.Input
/// </summary>
/// <param name="e">The element.</param>
/// <returns>True if the element can be focused.</returns>
private static bool CanFocus(IInputElement e) => e.Focusable && e.IsEnabledCore && e.IsVisible;
private static bool CanFocus(IInputElement e) => e.Focusable && e.IsEffectivelyEnabled && e.IsVisible;
/// <summary>
/// Gets the focus scope ancestors of the specified control, traversing popups.

127
src/Avalonia.Input/GestureRecognizers/GestureRecognizerCollection.cs

@ -0,0 +1,127 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.LogicalTree;
using Avalonia.Styling;
namespace Avalonia.Input.GestureRecognizers
{
public class GestureRecognizerCollection : IReadOnlyCollection<IGestureRecognizer>, IGestureRecognizerActionsDispatcher
{
private readonly IInputElement _inputElement;
private List<IGestureRecognizer> _recognizers;
private Dictionary<IPointer, IGestureRecognizer> _pointerGrabs;
public GestureRecognizerCollection(IInputElement inputElement)
{
_inputElement = inputElement;
}
public void Add(IGestureRecognizer recognizer)
{
if (_recognizers == null)
{
// We initialize the collection when the first recognizer is added
_recognizers = new List<IGestureRecognizer>();
_pointerGrabs = new Dictionary<IPointer, IGestureRecognizer>();
}
_recognizers.Add(recognizer);
recognizer.Initialize(_inputElement, this);
// Hacks to make bindings work
if (_inputElement is ILogical logicalParent && recognizer is ISetLogicalParent logical)
{
logical.SetParent(logicalParent);
if (recognizer is IStyleable styleableRecognizer
&& _inputElement is IStyleable styleableParent)
styleableRecognizer.Bind(StyledElement.TemplatedParentProperty,
styleableParent.GetObservable(StyledElement.TemplatedParentProperty));
}
}
static readonly List<IGestureRecognizer> s_Empty = new List<IGestureRecognizer>();
public IEnumerator<IGestureRecognizer> GetEnumerator()
=> _recognizers?.GetEnumerator() ?? s_Empty.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public int Count => _recognizers?.Count ?? 0;
internal bool HandlePointerPressed(PointerPressedEventArgs e)
{
if (_recognizers == null)
return false;
foreach (var r in _recognizers)
{
if(e.Handled)
break;
r.PointerPressed(e);
}
return e.Handled;
}
internal bool HandlePointerReleased(PointerReleasedEventArgs e)
{
if (_recognizers == null)
return false;
if (_pointerGrabs.TryGetValue(e.Pointer, out var capture))
{
capture.PointerReleased(e);
}
else
foreach (var r in _recognizers)
{
if (e.Handled)
break;
r.PointerReleased(e);
}
return e.Handled;
}
internal bool HandlePointerMoved(PointerEventArgs e)
{
if (_recognizers == null)
return false;
if (_pointerGrabs.TryGetValue(e.Pointer, out var capture))
{
capture.PointerMoved(e);
}
else
foreach (var r in _recognizers)
{
if (e.Handled)
break;
r.PointerMoved(e);
}
return e.Handled;
}
internal void HandlePointerCaptureLost(PointerCaptureLostEventArgs e)
{
if (_recognizers == null)
return;
_pointerGrabs.Remove(e.Pointer);
foreach (var r in _recognizers)
{
if(e.Handled)
break;
r.PointerCaptureLost(e);
}
}
void IGestureRecognizerActionsDispatcher.Capture(IPointer pointer, IGestureRecognizer recognizer)
{
pointer.Capture(_inputElement);
_pointerGrabs[pointer] = recognizer;
}
}
}

23
src/Avalonia.Input/GestureRecognizers/IGestureRecognizer.cs

@ -0,0 +1,23 @@
namespace Avalonia.Input.GestureRecognizers
{
public interface IGestureRecognizer
{
void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions);
void PointerPressed(PointerPressedEventArgs e);
void PointerReleased(PointerReleasedEventArgs e);
void PointerMoved(PointerEventArgs e);
void PointerCaptureLost(PointerCaptureLostEventArgs e);
}
public interface IGestureRecognizerActionsDispatcher
{
void Capture(IPointer pointer, IGestureRecognizer recognizer);
}
public enum GestureRecognizerResult
{
None,
Capture,
ReleaseCapture
}
}

183
src/Avalonia.Input/GestureRecognizers/ScrollGestureRecognizer.cs

@ -0,0 +1,183 @@
using System;
using System.Diagnostics;
using Avalonia.Interactivity;
using Avalonia.Threading;
namespace Avalonia.Input.GestureRecognizers
{
public class ScrollGestureRecognizer
: StyledElement, // It's not an "element" in any way, shape or form, but TemplateBinding refuse to work otherwise
IGestureRecognizer
{
private bool _scrolling;
private Point _trackedRootPoint;
private IPointer _tracking;
private IInputElement _target;
private IGestureRecognizerActionsDispatcher _actions;
private bool _canHorizontallyScroll;
private bool _canVerticallyScroll;
private int _gestureId;
// Movement per second
private Vector _inertia;
private ulong? _lastMoveTimestamp;
/// <summary>
/// Defines the <see cref="CanHorizontallyScroll"/> property.
/// </summary>
public static readonly DirectProperty<ScrollGestureRecognizer, bool> CanHorizontallyScrollProperty =
AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>(
nameof(CanHorizontallyScroll),
o => o.CanHorizontallyScroll,
(o, v) => o.CanHorizontallyScroll = v);
/// <summary>
/// Defines the <see cref="CanVerticallyScroll"/> property.
/// </summary>
public static readonly DirectProperty<ScrollGestureRecognizer, bool> CanVerticallyScrollProperty =
AvaloniaProperty.RegisterDirect<ScrollGestureRecognizer, bool>(
nameof(CanVerticallyScroll),
o => o.CanVerticallyScroll,
(o, v) => o.CanVerticallyScroll = v);
/// <summary>
/// Gets or sets a value indicating whether the content can be scrolled horizontally.
/// </summary>
public bool CanHorizontallyScroll
{
get => _canHorizontallyScroll;
set => SetAndRaise(CanHorizontallyScrollProperty, ref _canHorizontallyScroll, value);
}
/// <summary>
/// Gets or sets a value indicating whether the content can be scrolled horizontally.
/// </summary>
public bool CanVerticallyScroll
{
get => _canVerticallyScroll;
set => SetAndRaise(CanVerticallyScrollProperty, ref _canVerticallyScroll, value);
}
public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions)
{
_target = target;
_actions = actions;
}
public void PointerPressed(PointerPressedEventArgs e)
{
if (e.Pointer.IsPrimary && e.Pointer.Type == PointerType.Touch)
{
EndGesture();
_tracking = e.Pointer;
_gestureId = ScrollGestureEventArgs.GetNextFreeId();;
_trackedRootPoint = e.GetPosition(null);
}
}
// Arbitrary chosen value, probably need to move that to platform settings or something
private const double ScrollStartDistance = 30;
// Pixels per second speed that is considered to be the stop of inertiall scroll
private const double InertialScrollSpeedEnd = 5;
public void PointerMoved(PointerEventArgs e)
{
if (e.Pointer == _tracking)
{
var rootPoint = e.GetPosition(null);
if (!_scrolling)
{
if (CanHorizontallyScroll && Math.Abs(_trackedRootPoint.X - rootPoint.X) > ScrollStartDistance)
_scrolling = true;
if (CanVerticallyScroll && Math.Abs(_trackedRootPoint.Y - rootPoint.Y) > ScrollStartDistance)
_scrolling = true;
if (_scrolling)
{
_actions.Capture(e.Pointer, this);
}
}
if (_scrolling)
{
var vector = _trackedRootPoint - rootPoint;
var elapsed = _lastMoveTimestamp.HasValue ?
TimeSpan.FromMilliseconds(e.Timestamp - _lastMoveTimestamp.Value) :
TimeSpan.Zero;
_lastMoveTimestamp = e.Timestamp;
_trackedRootPoint = rootPoint;
if (elapsed.TotalSeconds > 0)
_inertia = vector / elapsed.TotalSeconds;
_target.RaiseEvent(new ScrollGestureEventArgs(_gestureId, vector));
e.Handled = true;
}
}
}
public void PointerCaptureLost(PointerCaptureLostEventArgs e)
{
if (e.Pointer == _tracking) EndGesture();
}
void EndGesture()
{
_tracking = null;
if (_scrolling)
{
_inertia = default;
_scrolling = false;
_target.RaiseEvent(new ScrollGestureEndedEventArgs(_gestureId));
_gestureId = 0;
_lastMoveTimestamp = null;
}
}
public void PointerReleased(PointerReleasedEventArgs e)
{
if (e.Pointer == _tracking && _scrolling)
{
e.Handled = true;
if (_inertia == default
|| e.Timestamp == 0
|| _lastMoveTimestamp == 0
|| e.Timestamp - _lastMoveTimestamp > 200)
EndGesture();
else
{
var savedGestureId = _gestureId;
var st = Stopwatch.StartNew();
var lastTime = TimeSpan.Zero;
DispatcherTimer.Run(() =>
{
// Another gesture has started, finish the current one
if (_gestureId != savedGestureId)
{
return false;
}
var elapsedSinceLastTick = st.Elapsed - lastTime;
lastTime = st.Elapsed;
var speed = _inertia * Math.Pow(0.15, st.Elapsed.TotalSeconds);
var distance = speed * elapsedSinceLastTick.TotalSeconds;
_target.RaiseEvent(new ScrollGestureEventArgs(_gestureId, distance));
if (Math.Abs(speed.X) < InertialScrollSpeedEnd || Math.Abs(speed.Y) <= InertialScrollSpeedEnd)
{
EndGesture();
return false;
}
return true;
}, TimeSpan.FromMilliseconds(16), DispatcherPriority.Background);
}
}
}
}
}

18
src/Avalonia.Input/Gestures.cs

@ -18,6 +18,14 @@ namespace Avalonia.Input
RoutingStrategies.Bubble,
typeof(Gestures));
public static readonly RoutedEvent<ScrollGestureEventArgs> ScrollGestureEvent =
RoutedEvent.Register<ScrollGestureEventArgs>(
"ScrollGesture", RoutingStrategies.Bubble, typeof(Gestures));
public static readonly RoutedEvent<ScrollGestureEventArgs> ScrollGestureEndedEvent =
RoutedEvent.Register<ScrollGestureEventArgs>(
"ScrollGestureEnded", RoutingStrategies.Bubble, typeof(Gestures));
private static WeakReference s_lastPress;
static Gestures()
@ -38,7 +46,10 @@ namespace Avalonia.Input
}
else if (s_lastPress?.IsAlive == true && e.ClickCount == 2 && s_lastPress.Target == e.Source)
{
e.Source.RaiseEvent(new RoutedEventArgs(DoubleTappedEvent));
if (!ev.Handled)
{
e.Source.RaiseEvent(new RoutedEventArgs(DoubleTappedEvent));
}
}
}
}
@ -51,7 +62,10 @@ namespace Avalonia.Input
if (s_lastPress?.IsAlive == true && s_lastPress.Target == e.Source)
{
((IInteractive)s_lastPress.Target).RaiseEvent(new RoutedEventArgs(TappedEvent));
if (!ev.Handled)
{
((IInteractive)s_lastPress.Target).RaiseEvent(new RoutedEventArgs(TappedEvent));
}
}
}
}

6
src/Avalonia.Input/IInputElement.cs

@ -83,14 +83,14 @@ namespace Avalonia.Input
Cursor Cursor { get; }
/// <summary>
/// Gets a value indicating whether the control is effectively enabled for user interaction.
/// Gets a value indicating whether this control and all its parents are enabled.
/// </summary>
/// <remarks>
/// The <see cref="IsEnabled"/> property is used to toggle the enabled state for individual
/// controls. The <see cref="IsEnabledCore"/> property takes into account the
/// controls. The <see cref="IsEffectivelyEnabled"/> property takes into account the
/// <see cref="IsEnabled"/> value of this control and its parent controls.
/// </remarks>
bool IsEnabledCore { get; }
bool IsEffectivelyEnabled { get; }
/// <summary>
/// Gets a value indicating whether the control is focused.

5
src/Avalonia.Input/IMouseDevice.cs

@ -1,6 +1,8 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
namespace Avalonia.Input
{
/// <summary>
@ -11,6 +13,9 @@ namespace Avalonia.Input
/// <summary>
/// Gets the mouse position, in screen coordinates.
/// </summary>
[Obsolete("Use PointerEventArgs.GetPosition")]
PixelPoint Position { get; }
void SceneInvalidated(IInputRoot root, Rect rect);
}
}

18
src/Avalonia.Input/IPointer.cs

@ -0,0 +1,18 @@
namespace Avalonia.Input
{
public interface IPointer
{
int Id { get; }
void Capture(IInputElement control);
IInputElement Captured { get; }
PointerType Type { get; }
bool IsPrimary { get; }
}
public enum PointerType
{
Mouse,
Touch
}
}

8
src/Avalonia.Input/IPointerDevice.cs

@ -1,18 +1,20 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.VisualTree;
namespace Avalonia.Input
{
public interface IPointerDevice : IInputDevice
{
[Obsolete("Use IPointer")]
IInputElement Captured { get; }
[Obsolete("Use IPointer")]
void Capture(IInputElement control);
[Obsolete("Use PointerEventArgs.GetPosition")]
Point GetPosition(IVisual relativeTo);
void SceneInvalidated(IInputRoot root, Rect rect);
}
}

120
src/Avalonia.Input/InputElement.cs

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Input.GestureRecognizers;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
@ -27,10 +28,12 @@ namespace Avalonia.Input
AvaloniaProperty.Register<InputElement, bool>(nameof(IsEnabled), true);
/// <summary>
/// Defines the <see cref="IsEnabledCore"/> property.
/// Defines the <see cref="IsEffectivelyEnabled"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsEnabledCoreProperty =
AvaloniaProperty.Register<InputElement, bool>(nameof(IsEnabledCore), true);
public static readonly DirectProperty<InputElement, bool> IsEffectivelyEnabledProperty =
AvaloniaProperty.RegisterDirect<InputElement, bool>(
nameof(IsEffectivelyEnabled),
o => o.IsEffectivelyEnabled);
/// <summary>
/// Gets or sets associated mouse cursor.
@ -127,6 +130,14 @@ namespace Avalonia.Input
RoutedEvent.Register<InputElement, PointerReleasedEventArgs>(
"PointerReleased",
RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="PointerCaptureLost"/> routed event.
/// </summary>
public static readonly RoutedEvent<PointerCaptureLostEventArgs> PointerCaptureLostEvent =
RoutedEvent.Register<InputElement, PointerCaptureLostEventArgs>(
"PointerCaptureLost",
RoutingStrategies.Direct);
/// <summary>
/// Defines the <see cref="PointerWheelChanged"/> event.
@ -146,8 +157,10 @@ namespace Avalonia.Input
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> DoubleTappedEvent = Gestures.DoubleTappedEvent;
private bool _isEffectivelyEnabled = true;
private bool _isFocused;
private bool _isPointerOver;
private GestureRecognizerCollection _gestureRecognizers;
/// <summary>
/// Initializes static members of the <see cref="InputElement"/> class.
@ -166,9 +179,10 @@ namespace Avalonia.Input
PointerMovedEvent.AddClassHandler<InputElement>(x => x.OnPointerMoved);
PointerPressedEvent.AddClassHandler<InputElement>(x => x.OnPointerPressed);
PointerReleasedEvent.AddClassHandler<InputElement>(x => x.OnPointerReleased);
PointerCaptureLostEvent.AddClassHandler<InputElement>(x => x.OnPointerCaptureLost);
PointerWheelChangedEvent.AddClassHandler<InputElement>(x => x.OnPointerWheelChanged);
PseudoClass<InputElement, bool>(IsEnabledCoreProperty, x => !x, ":disabled");
PseudoClass<InputElement, bool>(IsEffectivelyEnabledProperty, x => !x, ":disabled");
PseudoClass<InputElement>(IsFocusedProperty, ":focus");
PseudoClass<InputElement>(IsPointerOverProperty, ":pointerover");
}
@ -263,6 +277,16 @@ namespace Avalonia.Input
remove { RemoveHandler(PointerReleasedEvent, value); }
}
/// <summary>
/// Occurs when the control or its child control loses the pointer capture for any reason,
/// event will not be triggered for a parent control if capture was transferred to another child of that parent control
/// </summary>
public event EventHandler<PointerCaptureLostEventArgs> PointerCaptureLost
{
add => AddHandler(PointerCaptureLostEvent, value);
remove => RemoveHandler(PointerCaptureLostEvent, value);
}
/// <summary>
/// Occurs when the mouse wheen is scrolled over the control.
/// </summary>
@ -344,31 +368,28 @@ namespace Avalonia.Input
internal set { SetAndRaise(IsPointerOverProperty, ref _isPointerOver, value); }
}
/// <summary>
/// Gets a value indicating whether the control is effectively enabled for user interaction.
/// </summary>
/// <remarks>
/// The <see cref="IsEnabled"/> property is used to toggle the enabled state for individual
/// controls. The <see cref="IsEnabledCore"/> property takes into account the
/// <see cref="IsEnabled"/> value of this control and its parent controls.
/// </remarks>
bool IInputElement.IsEnabledCore => IsEnabledCore;
/// <inheritdoc/>
public bool IsEffectivelyEnabled
{
get => _isEffectivelyEnabled;
private set => SetAndRaise(IsEffectivelyEnabledProperty, ref _isEffectivelyEnabled, value);
}
public List<KeyBinding> KeyBindings { get; } = new List<KeyBinding>();
/// <summary>
/// Gets a value indicating whether the control is effectively enabled for user interaction.
/// Allows a derived class to override the enabled state of the control.
/// </summary>
/// <remarks>
/// The <see cref="IsEnabled"/> property is used to toggle the enabled state for individual
/// controls. The <see cref="IsEnabledCore"/> property takes into account the
/// <see cref="IsEnabled"/> value of this control and its parent controls.
/// Derived controls may wish to disable the enabled state of the control without overwriting the
/// user-supplied <see cref="IsEnabled"/> setting. This can be done by overriding this property
/// to return the overridden enabled state. If the value returned from <see cref="IsEnabledCore"/>
/// should change, then the derived control should call <see cref="UpdateIsEffectivelyEnabled()"/>.
/// </remarks>
protected bool IsEnabledCore
{
get { return GetValue(IsEnabledCoreProperty); }
set { SetValue(IsEnabledCoreProperty, value); }
}
protected virtual bool IsEnabledCore => IsEnabled;
public List<KeyBinding> KeyBindings { get; } = new List<KeyBinding>();
public GestureRecognizerCollection GestureRecognizers
=> _gestureRecognizers ?? (_gestureRecognizers = new GestureRecognizerCollection(this));
/// <summary>
/// Focuses the control.
@ -393,7 +414,7 @@ namespace Avalonia.Input
protected override void OnAttachedToVisualTreeCore(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTreeCore(e);
UpdateIsEnabledCore();
UpdateIsEffectivelyEnabled();
}
/// <summary>
@ -460,6 +481,8 @@ namespace Avalonia.Input
/// <param name="e">The event args.</param>
protected virtual void OnPointerMoved(PointerEventArgs e)
{
if (_gestureRecognizers?.HandlePointerMoved(e) == true)
e.Handled = true;
}
/// <summary>
@ -468,6 +491,8 @@ namespace Avalonia.Input
/// <param name="e">The event args.</param>
protected virtual void OnPointerPressed(PointerPressedEventArgs e)
{
if (_gestureRecognizers?.HandlePointerPressed(e) == true)
e.Handled = true;
}
/// <summary>
@ -476,6 +501,17 @@ namespace Avalonia.Input
/// <param name="e">The event args.</param>
protected virtual void OnPointerReleased(PointerReleasedEventArgs e)
{
if (_gestureRecognizers?.HandlePointerReleased(e) == true)
e.Handled = true;
}
/// <summary>
/// Called before the <see cref="PointerCaptureLost"/> event occurs.
/// </summary>
/// <param name="e">The event args.</param>
protected virtual void OnPointerCaptureLost(PointerCaptureLostEventArgs e)
{
_gestureRecognizers?.HandlePointerCaptureLost(e);
}
/// <summary>
@ -486,9 +522,18 @@ namespace Avalonia.Input
{
}
/// <summary>
/// Updates the <see cref="IsEffectivelyEnabled"/> property value according to the parent
/// control's enabled state and <see cref="IsEnabledCore"/>.
/// </summary>
protected void UpdateIsEffectivelyEnabled()
{
UpdateIsEffectivelyEnabled(this.GetVisualParent<InputElement>());
}
private static void IsEnabledChanged(AvaloniaPropertyChangedEventArgs e)
{
((InputElement)e.Sender).UpdateIsEnabledCore();
((InputElement)e.Sender).UpdateIsEffectivelyEnabled();
}
/// <summary>
@ -512,32 +557,17 @@ namespace Avalonia.Input
}
/// <summary>
/// Updates the <see cref="IsEnabledCore"/> property value.
/// </summary>
private void UpdateIsEnabledCore()
{
UpdateIsEnabledCore(this.GetVisualParent<InputElement>());
}
/// <summary>
/// Updates the <see cref="IsEnabledCore"/> property based on the parent's
/// <see cref="IsEnabledCore"/>.
/// Updates the <see cref="IsEffectivelyEnabled"/> property based on the parent's
/// <see cref="IsEffectivelyEnabled"/>.
/// </summary>
/// <param name="parent">The parent control.</param>
private void UpdateIsEnabledCore(InputElement parent)
private void UpdateIsEffectivelyEnabled(InputElement parent)
{
if (parent != null)
{
IsEnabledCore = IsEnabled && parent.IsEnabledCore;
}
else
{
IsEnabledCore = IsEnabled;
}
IsEffectivelyEnabled = IsEnabledCore && (parent?.IsEffectivelyEnabled ?? true);
foreach (var child in this.GetVisualChildren().OfType<InputElement>())
{
child.UpdateIsEnabledCore(this);
child.UpdateIsEffectivelyEnabled(this);
}
}
}

2
src/Avalonia.Input/InputExtensions.cs

@ -45,7 +45,7 @@ namespace Avalonia.Input
return element != null &&
element.IsVisible &&
element.IsHitTestVisible &&
element.IsEnabledCore &&
element.IsEffectivelyEnabled &&
element.IsAttachedToVisualTree;
}
}

227
src/Avalonia.Input/MouseDevice.cs

@ -19,9 +19,14 @@ namespace Avalonia.Input
private int _clickCount;
private Rect _lastClickRect;
private ulong _lastClickTime;
private IInputElement _captured;
private IDisposable _capturedSubscription;
private readonly Pointer _pointer;
public MouseDevice(Pointer pointer = null)
{
_pointer = pointer ?? new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
}
/// <summary>
/// Gets the control that is currently capturing by the mouse, if any.
/// </summary>
@ -30,27 +35,9 @@ namespace Avalonia.Input
/// within the control's bounds or not. To set the mouse capture, call the
/// <see cref="Capture"/> method.
/// </remarks>
public IInputElement Captured
{
get => _captured;
protected set
{
_capturedSubscription?.Dispose();
_capturedSubscription = null;
[Obsolete("Use IPointer instead")]
public IInputElement Captured => _pointer.Captured;
if (value != null)
{
_capturedSubscription = Observable.FromEventPattern<VisualTreeAttachmentEventArgs>(
x => value.DetachedFromVisualTree += x,
x => value.DetachedFromVisualTree -= x)
.Take(1)
.Subscribe(_ => Captured = null);
}
_captured = value;
}
}
/// <summary>
/// Gets the mouse position, in screen coordinates.
/// </summary>
@ -69,10 +56,9 @@ namespace Avalonia.Input
/// within the control's bounds or not. The current mouse capture control is exposed
/// by the <see cref="Captured"/> property.
/// </remarks>
public virtual void Capture(IInputElement control)
public void Capture(IInputElement control)
{
// TODO: Check visibility and enabled state before setting capture.
Captured = control;
_pointer.Capture(control);
}
/// <summary>
@ -96,7 +82,7 @@ namespace Avalonia.Input
public void ProcessRawEvent(RawInputEventArgs e)
{
if (!e.Handled && e is RawMouseEventArgs margs)
if (!e.Handled && e is RawPointerEventArgs margs)
ProcessRawEvent(margs);
}
@ -106,66 +92,100 @@ namespace Avalonia.Input
if (rect.Contains(clientPoint))
{
if (Captured == null)
if (_pointer.Captured == null)
{
SetPointerOver(this, root, clientPoint);
SetPointerOver(this, 0 /* TODO: proper timestamp */, root, clientPoint, InputModifiers.None);
}
else
{
SetPointerOver(this, root, Captured);
SetPointerOver(this, 0 /* TODO: proper timestamp */, root, _pointer.Captured, InputModifiers.None);
}
}
}
private void ProcessRawEvent(RawMouseEventArgs e)
int ButtonCount(PointerPointProperties props)
{
var rv = 0;
if (props.IsLeftButtonPressed)
rv++;
if (props.IsMiddleButtonPressed)
rv++;
if (props.IsRightButtonPressed)
rv++;
return rv;
}
private void ProcessRawEvent(RawPointerEventArgs e)
{
Contract.Requires<ArgumentNullException>(e != null);
var mouse = (IMouseDevice)e.Device;
Position = e.Root.PointToScreen(e.Position);
var props = CreateProperties(e);
switch (e.Type)
{
case RawMouseEventType.LeaveWindow:
LeaveWindow(mouse, e.Root);
case RawPointerEventType.LeaveWindow:
LeaveWindow(mouse, e.Timestamp, e.Root, e.InputModifiers);
break;
case RawMouseEventType.LeftButtonDown:
case RawMouseEventType.RightButtonDown:
case RawMouseEventType.MiddleButtonDown:
e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position,
e.Type == RawMouseEventType.LeftButtonDown
? MouseButton.Left
: e.Type == RawMouseEventType.RightButtonDown ? MouseButton.Right : MouseButton.Middle,
e.InputModifiers);
case RawPointerEventType.LeftButtonDown:
case RawPointerEventType.RightButtonDown:
case RawPointerEventType.MiddleButtonDown:
if (ButtonCount(props) > 1)
e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, e.InputModifiers);
else
e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position,
props, e.InputModifiers);
break;
case RawMouseEventType.LeftButtonUp:
case RawMouseEventType.RightButtonUp:
case RawMouseEventType.MiddleButtonUp:
e.Handled = MouseUp(mouse, e.Root, e.Position,
e.Type == RawMouseEventType.LeftButtonUp
? MouseButton.Left
: e.Type == RawMouseEventType.RightButtonUp ? MouseButton.Right : MouseButton.Middle,
e.InputModifiers);
case RawPointerEventType.LeftButtonUp:
case RawPointerEventType.RightButtonUp:
case RawPointerEventType.MiddleButtonUp:
if (ButtonCount(props) != 0)
e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, e.InputModifiers);
else
e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, e.InputModifiers);
break;
case RawMouseEventType.Move:
e.Handled = MouseMove(mouse, e.Root, e.Position, e.InputModifiers);
case RawPointerEventType.Move:
e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, e.InputModifiers);
break;
case RawMouseEventType.Wheel:
e.Handled = MouseWheel(mouse, e.Root, e.Position, ((RawMouseWheelEventArgs)e).Delta, e.InputModifiers);
case RawPointerEventType.Wheel:
e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, e.InputModifiers);
break;
}
}
private void LeaveWindow(IMouseDevice device, IInputRoot root)
private void LeaveWindow(IMouseDevice device, ulong timestamp, IInputRoot root, InputModifiers inputModifiers)
{
Contract.Requires<ArgumentNullException>(device != null);
Contract.Requires<ArgumentNullException>(root != null);
ClearPointerOver(this, root);
ClearPointerOver(this, timestamp, root, inputModifiers);
}
private bool MouseDown(IMouseDevice device, ulong timestamp, IInputElement root, Point p, MouseButton button, InputModifiers inputModifiers)
PointerPointProperties CreateProperties(RawPointerEventArgs args)
{
var rv = new PointerPointProperties(args.InputModifiers);
if (args.Type == RawPointerEventType.LeftButtonDown)
rv.IsLeftButtonPressed = true;
if (args.Type == RawPointerEventType.MiddleButtonDown)
rv.IsMiddleButtonPressed = true;
if (args.Type == RawPointerEventType.RightButtonDown)
rv.IsRightButtonPressed = true;
if (args.Type == RawPointerEventType.LeftButtonUp)
rv.IsLeftButtonPressed = false;
if (args.Type == RawPointerEventType.MiddleButtonUp)
rv.IsMiddleButtonPressed = false;
if (args.Type == RawPointerEventType.RightButtonUp)
rv.IsRightButtonPressed = false;
return rv;
}
private MouseButton _lastMouseDownButton;
private bool MouseDown(IMouseDevice device, ulong timestamp, IInputElement root, Point p,
PointerPointProperties properties,
InputModifiers inputModifiers)
{
Contract.Requires<ArgumentNullException>(device != null);
Contract.Requires<ArgumentNullException>(root != null);
@ -174,8 +194,8 @@ namespace Avalonia.Input
if (hit != null)
{
IInteractive source = GetSource(hit);
_pointer.Capture(hit);
var source = GetSource(hit);
if (source != null)
{
var settings = AvaloniaLocator.Current.GetService<IPlatformSettings>();
@ -190,17 +210,8 @@ namespace Avalonia.Input
_lastClickTime = timestamp;
_lastClickRect = new Rect(p, new Size())
.Inflate(new Thickness(settings.DoubleClickSize.Width / 2, settings.DoubleClickSize.Height / 2));
var e = new PointerPressedEventArgs
{
Device = this,
RoutedEvent = InputElement.PointerPressedEvent,
Source = source,
ClickCount = _clickCount,
MouseButton = button,
InputModifiers = inputModifiers
};
_lastMouseDownButton = properties.GetObsoleteMouseButton();
var e = new PointerPressedEventArgs(source, _pointer, root, p, timestamp, properties, inputModifiers, _clickCount);
source.RaiseEvent(e);
return e.Handled;
}
@ -209,36 +220,33 @@ namespace Avalonia.Input
return false;
}
private bool MouseMove(IMouseDevice device, IInputRoot root, Point p, InputModifiers inputModifiers)
private bool MouseMove(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties properties,
InputModifiers inputModifiers)
{
Contract.Requires<ArgumentNullException>(device != null);
Contract.Requires<ArgumentNullException>(root != null);
IInputElement source;
if (Captured == null)
if (_pointer.Captured == null)
{
source = SetPointerOver(this, root, p);
source = SetPointerOver(this, timestamp, root, p, inputModifiers);
}
else
{
SetPointerOver(this, root, Captured);
source = Captured;
SetPointerOver(this, timestamp, root, _pointer.Captured, inputModifiers);
source = _pointer.Captured;
}
var e = new PointerEventArgs
{
Device = this,
RoutedEvent = InputElement.PointerMovedEvent,
Source = source,
InputModifiers = inputModifiers
};
var e = new PointerEventArgs(InputElement.PointerMovedEvent, source, _pointer, root,
p, timestamp, properties, inputModifiers);
source?.RaiseEvent(e);
return e.Handled;
}
private bool MouseUp(IMouseDevice device, IInputRoot root, Point p, MouseButton button, InputModifiers inputModifiers)
private bool MouseUp(IMouseDevice device, ulong timestamp, IInputRoot root, Point p, PointerPointProperties props,
InputModifiers inputModifiers)
{
Contract.Requires<ArgumentNullException>(device != null);
Contract.Requires<ArgumentNullException>(root != null);
@ -248,23 +256,20 @@ namespace Avalonia.Input
if (hit != null)
{
var source = GetSource(hit);
var e = new PointerReleasedEventArgs
{
Device = this,
RoutedEvent = InputElement.PointerReleasedEvent,
Source = source,
MouseButton = button,
InputModifiers = inputModifiers
};
var e = new PointerReleasedEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers,
_lastMouseDownButton);
source?.RaiseEvent(e);
_pointer.Capture(null);
return e.Handled;
}
return false;
}
private bool MouseWheel(IMouseDevice device, IInputRoot root, Point p, Vector delta, InputModifiers inputModifiers)
private bool MouseWheel(IMouseDevice device, ulong timestamp, IInputRoot root, Point p,
PointerPointProperties props,
Vector delta, InputModifiers inputModifiers)
{
Contract.Requires<ArgumentNullException>(device != null);
Contract.Requires<ArgumentNullException>(root != null);
@ -274,14 +279,7 @@ namespace Avalonia.Input
if (hit != null)
{
var source = GetSource(hit);
var e = new PointerWheelEventArgs
{
Device = this,
RoutedEvent = InputElement.PointerWheelChangedEvent,
Source = source,
Delta = delta,
InputModifiers = inputModifiers
};
var e = new PointerWheelEventArgs(source, _pointer, root, p, timestamp, props, inputModifiers, delta);
source?.RaiseEvent(e);
return e.Handled;
@ -294,7 +292,7 @@ namespace Avalonia.Input
{
Contract.Requires<ArgumentNullException>(hit != null);
return Captured ??
return _pointer.Captured ??
(hit as IInteractive) ??
hit.GetSelfAndVisualAncestors().OfType<IInteractive>().FirstOrDefault();
}
@ -303,20 +301,22 @@ namespace Avalonia.Input
{
Contract.Requires<ArgumentNullException>(root != null);
return Captured ?? root.InputHitTest(p);
return _pointer.Captured ?? root.InputHitTest(p);
}
private void ClearPointerOver(IPointerDevice device, IInputRoot root)
PointerEventArgs CreateSimpleEvent(RoutedEvent ev, ulong timestamp, IInteractive source, InputModifiers inputModifiers)
{
return new PointerEventArgs(ev, source, _pointer, null, default,
timestamp, new PointerPointProperties(inputModifiers), inputModifiers);
}
private void ClearPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, InputModifiers inputModifiers)
{
Contract.Requires<ArgumentNullException>(device != null);
Contract.Requires<ArgumentNullException>(root != null);
var element = root.PointerOverElement;
var e = new PointerEventArgs
{
RoutedEvent = InputElement.PointerLeaveEvent,
Device = device,
};
var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, element, inputModifiers);
if (element!=null && !element.IsAttachedToVisualTree)
{
@ -353,7 +353,7 @@ namespace Avalonia.Input
}
}
private IInputElement SetPointerOver(IPointerDevice device, IInputRoot root, Point p)
private IInputElement SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, Point p, InputModifiers inputModifiers)
{
Contract.Requires<ArgumentNullException>(device != null);
Contract.Requires<ArgumentNullException>(root != null);
@ -364,18 +364,18 @@ namespace Avalonia.Input
{
if (element != null)
{
SetPointerOver(device, root, element);
SetPointerOver(device, timestamp, root, element, inputModifiers);
}
else
{
ClearPointerOver(device, root);
ClearPointerOver(device, timestamp, root, inputModifiers);
}
}
return element;
}
private void SetPointerOver(IPointerDevice device, IInputRoot root, IInputElement element)
private void SetPointerOver(IPointerDevice device, ulong timestamp, IInputRoot root, IInputElement element, InputModifiers inputModifiers)
{
Contract.Requires<ArgumentNullException>(device != null);
Contract.Requires<ArgumentNullException>(root != null);
@ -383,7 +383,6 @@ namespace Avalonia.Input
IInputElement branch = null;
var e = new PointerEventArgs { Device = device, };
var el = element;
while (el != null)
@ -397,8 +396,8 @@ namespace Avalonia.Input
}
el = root.PointerOverElement;
e.RoutedEvent = InputElement.PointerLeaveEvent;
var e = CreateSimpleEvent(InputElement.PointerLeaveEvent, timestamp, el, inputModifiers);
if (el!=null && branch!=null && !el.IsAttachedToVisualTree)
{
ClearChildrenPointerOver(e,branch,false);

4
src/Avalonia.Input/Navigation/FocusExtensions.cs

@ -13,13 +13,13 @@ namespace Avalonia.Input.Navigation
/// </summary>
/// <param name="e">The element.</param>
/// <returns>True if the element can be focused.</returns>
public static bool CanFocus(this IInputElement e) => e.Focusable && e.IsEnabledCore && e.IsVisible;
public static bool CanFocus(this IInputElement e) => e.Focusable && e.IsEffectivelyEnabled && e.IsVisible;
/// <summary>
/// Checks if descendants of the specified element can be focused.
/// </summary>
/// <param name="e">The element.</param>
/// <returns>True if descendants of the element can be focused.</returns>
public static bool CanFocusDescendants(this IInputElement e) => e.IsEnabledCore && e.IsVisible;
public static bool CanFocusDescendants(this IInputElement e) => e.IsEffectivelyEnabled && e.IsVisible;
}
}

73
src/Avalonia.Input/Pointer.cs

@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
namespace Avalonia.Input
{
public class Pointer : IPointer, IDisposable
{
private static int s_NextFreePointerId = 1000;
public static int GetNextFreeId() => s_NextFreePointerId++;
public Pointer(int id, PointerType type, bool isPrimary)
{
Id = id;
Type = type;
IsPrimary = isPrimary;
}
public int Id { get; }
IInputElement FindCommonParent(IInputElement control1, IInputElement control2)
{
if (control1 == null || control2 == null)
return null;
var seen = new HashSet<IInputElement>(control1.GetSelfAndVisualAncestors().OfType<IInputElement>());
return control2.GetSelfAndVisualAncestors().OfType<IInputElement>().FirstOrDefault(seen.Contains);
}
protected virtual void PlatformCapture(IInputElement element)
{
}
public void Capture(IInputElement control)
{
if (Captured != null)
Captured.DetachedFromVisualTree -= OnCaptureDetached;
var oldCapture = control;
Captured = control;
PlatformCapture(control);
if (oldCapture != null)
{
var commonParent = FindCommonParent(control, oldCapture);
foreach (var notifyTarget in oldCapture.GetSelfAndVisualAncestors().OfType<IInputElement>())
{
if (notifyTarget == commonParent)
break;
notifyTarget.RaiseEvent(new PointerCaptureLostEventArgs(notifyTarget, this));
}
}
if (Captured != null)
Captured.DetachedFromVisualTree += OnCaptureDetached;
}
IInputElement GetNextCapture(IVisual parent) =>
parent as IInputElement ?? parent.GetVisualAncestors().OfType<IInputElement>().FirstOrDefault();
private void OnCaptureDetached(object sender, VisualTreeAttachmentEventArgs e)
{
Capture(GetNextCapture(e.Parent));
}
public IInputElement Captured { get; private set; }
public PointerType Type { get; }
public bool IsPrimary { get; }
public void Dispose() => Capture(null);
}
}

108
src/Avalonia.Input/PointerEventArgs.cs

@ -1,6 +1,8 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Input.Raw;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
@ -8,25 +10,69 @@ namespace Avalonia.Input
{
public class PointerEventArgs : RoutedEventArgs
{
public PointerEventArgs()
{
private readonly IVisual _rootVisual;
private readonly Point _rootVisualPosition;
private readonly PointerPointProperties _properties;
public PointerEventArgs(RoutedEvent routedEvent,
IInteractive source,
IPointer pointer,
IVisual rootVisual, Point rootVisualPosition,
ulong timestamp,
PointerPointProperties properties,
InputModifiers modifiers)
: base(routedEvent)
{
Source = source;
_rootVisual = rootVisual;
_rootVisualPosition = rootVisualPosition;
_properties = properties;
Pointer = pointer;
Timestamp = timestamp;
InputModifiers = modifiers;
}
public PointerEventArgs(RoutedEvent routedEvent)
: base(routedEvent)
class EmulatedDevice : IPointerDevice
{
private readonly PointerEventArgs _ev;
public EmulatedDevice(PointerEventArgs ev)
{
_ev = ev;
}
public void ProcessRawEvent(RawInputEventArgs ev) => throw new NotSupportedException();
public IInputElement Captured => _ev.Pointer.Captured;
public void Capture(IInputElement control)
{
_ev.Pointer.Capture(control);
}
public Point GetPosition(IVisual relativeTo) => _ev.GetPosition(relativeTo);
}
public IPointerDevice Device { get; set; }
public IPointer Pointer { get; }
public ulong Timestamp { get; }
private IPointerDevice _device;
public InputModifiers InputModifiers { get; set; }
[Obsolete("Use Pointer to get pointer-specific information")]
public IPointerDevice Device => _device ?? (_device = new EmulatedDevice(this));
public InputModifiers InputModifiers { get; }
public Point GetPosition(IVisual relativeTo)
{
return Device.GetPosition(relativeTo);
if (_rootVisual == null)
return default;
if (relativeTo == null)
return _rootVisualPosition;
return _rootVisualPosition * _rootVisual.TransformToVisual(relativeTo) ?? default;
}
public PointerPoint GetPointerPoint(IVisual relativeTo)
=> new PointerPoint(Pointer, GetPosition(relativeTo), _properties);
}
public enum MouseButton
@ -39,32 +85,52 @@ namespace Avalonia.Input
public class PointerPressedEventArgs : PointerEventArgs
{
public PointerPressedEventArgs()
: base(InputElement.PointerPressedEvent)
{
}
private readonly int _obsoleteClickCount;
public PointerPressedEventArgs(RoutedEvent routedEvent)
: base(routedEvent)
public PointerPressedEventArgs(
IInteractive source,
IPointer pointer,
IVisual rootVisual, Point rootVisualPosition,
ulong timestamp,
PointerPointProperties properties,
InputModifiers modifiers,
int obsoleteClickCount = 1)
: base(InputElement.PointerPressedEvent, source, pointer, rootVisual, rootVisualPosition,
timestamp, properties, modifiers)
{
_obsoleteClickCount = obsoleteClickCount;
}
public int ClickCount { get; set; }
public MouseButton MouseButton { get; set; }
[Obsolete("Use DoubleTapped or DoubleRightTapped event instead")]
public int ClickCount => _obsoleteClickCount;
[Obsolete] public MouseButton MouseButton => GetPointerPoint(null).Properties.GetObsoleteMouseButton();
}
public class PointerReleasedEventArgs : PointerEventArgs
{
public PointerReleasedEventArgs()
: base(InputElement.PointerReleasedEvent)
public PointerReleasedEventArgs(
IInteractive source, IPointer pointer,
IVisual rootVisual, Point rootVisualPosition, ulong timestamp,
PointerPointProperties properties, InputModifiers modifiers, MouseButton obsoleteMouseButton)
: base(InputElement.PointerReleasedEvent, source, pointer, rootVisual, rootVisualPosition,
timestamp, properties, modifiers)
{
MouseButton = obsoleteMouseButton;
}
public PointerReleasedEventArgs(RoutedEvent routedEvent)
: base(routedEvent)
[Obsolete()]
public MouseButton MouseButton { get; private set; }
}
public class PointerCaptureLostEventArgs : RoutedEventArgs
{
public IPointer Pointer { get; }
public PointerCaptureLostEventArgs(IInteractive source, IPointer pointer) : base(InputElement.PointerCaptureLostEvent)
{
Pointer = pointer;
Source = source;
}
public MouseButton MouseButton { get; set; }
}
}

45
src/Avalonia.Input/PointerPoint.cs

@ -0,0 +1,45 @@
namespace Avalonia.Input
{
public sealed class PointerPoint
{
public PointerPoint(IPointer pointer, Point position, PointerPointProperties properties)
{
Pointer = pointer;
Position = position;
Properties = properties;
}
public IPointer Pointer { get; }
public PointerPointProperties Properties { get; }
public Point Position { get; }
}
public sealed class PointerPointProperties
{
public bool IsLeftButtonPressed { get; set; }
public bool IsMiddleButtonPressed { get; set; }
public bool IsRightButtonPressed { get; set; }
public PointerPointProperties()
{
}
public PointerPointProperties(InputModifiers modifiers)
{
IsLeftButtonPressed = modifiers.HasFlag(InputModifiers.LeftMouseButton);
IsMiddleButtonPressed = modifiers.HasFlag(InputModifiers.MiddleMouseButton);
IsRightButtonPressed = modifiers.HasFlag(InputModifiers.RightMouseButton);
}
public MouseButton GetObsoleteMouseButton()
{
if (IsLeftButtonPressed)
return MouseButton.Left;
if (IsMiddleButtonPressed)
return MouseButton.Middle;
if (IsRightButtonPressed)
return MouseButton.Right;
return MouseButton.None;
}
}
}

12
src/Avalonia.Input/PointerWheelEventArgs.cs

@ -1,10 +1,22 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using Avalonia.Interactivity;
using Avalonia.VisualTree;
namespace Avalonia.Input
{
public class PointerWheelEventArgs : PointerEventArgs
{
public Vector Delta { get; set; }
public PointerWheelEventArgs(IInteractive source, IPointer pointer, IVisual rootVisual,
Point rootVisualPosition, ulong timestamp,
PointerPointProperties properties, InputModifiers modifiers, Vector delta)
: base(InputElement.PointerWheelChangedEvent, source, pointer, rootVisual, rootVisualPosition,
timestamp, properties, modifiers)
{
Delta = delta;
}
}
}

1
src/Avalonia.Input/Properties/AssemblyInfo.cs

@ -5,3 +5,4 @@ using System.Reflection;
using Avalonia.Metadata;
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Input.GestureRecognizers")]

4
src/Avalonia.Input/Raw/RawMouseWheelEventArgs.cs

@ -4,7 +4,7 @@
namespace Avalonia.Input.Raw
{
public class RawMouseWheelEventArgs : RawMouseEventArgs
public class RawMouseWheelEventArgs : RawPointerEventArgs
{
public RawMouseWheelEventArgs(
IInputDevice device,
@ -12,7 +12,7 @@ namespace Avalonia.Input.Raw
IInputRoot root,
Point position,
Vector delta, InputModifiers inputModifiers)
: base(device, timestamp, root, RawMouseEventType.Wheel, position, inputModifiers)
: base(device, timestamp, root, RawPointerEventType.Wheel, position, inputModifiers)
{
Delta = delta;
}

15
src/Avalonia.Input/Raw/RawMouseEventArgs.cs → src/Avalonia.Input/Raw/RawPointerEventArgs.cs

@ -5,7 +5,7 @@ using System;
namespace Avalonia.Input.Raw
{
public enum RawMouseEventType
public enum RawPointerEventType
{
LeaveWindow,
LeftButtonDown,
@ -17,15 +17,18 @@ namespace Avalonia.Input.Raw
Move,
Wheel,
NonClientLeftButtonDown,
TouchBegin,
TouchUpdate,
TouchEnd
}
/// <summary>
/// A raw mouse event.
/// </summary>
public class RawMouseEventArgs : RawInputEventArgs
public class RawPointerEventArgs : RawInputEventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="RawMouseEventArgs"/> class.
/// Initializes a new instance of the <see cref="RawPointerEventArgs"/> class.
/// </summary>
/// <param name="device">The associated device.</param>
/// <param name="timestamp">The event timestamp.</param>
@ -33,11 +36,11 @@ namespace Avalonia.Input.Raw
/// <param name="type">The type of the event.</param>
/// <param name="position">The mouse position, in client DIPs.</param>
/// <param name="inputModifiers">The input modifiers.</param>
public RawMouseEventArgs(
public RawPointerEventArgs(
IInputDevice device,
ulong timestamp,
IInputRoot root,
RawMouseEventType type,
RawPointerEventType type,
Point position,
InputModifiers inputModifiers)
: base(device, timestamp)
@ -64,7 +67,7 @@ namespace Avalonia.Input.Raw
/// <summary>
/// Gets the type of the event.
/// </summary>
public RawMouseEventType Type { get; private set; }
public RawPointerEventType Type { get; private set; }
/// <summary>
/// Gets the input modifiers.

15
src/Avalonia.Input/Raw/RawTouchEventArgs.cs

@ -0,0 +1,15 @@
namespace Avalonia.Input.Raw
{
public class RawTouchEventArgs : RawPointerEventArgs
{
public RawTouchEventArgs(IInputDevice device, ulong timestamp, IInputRoot root,
RawPointerEventType type, Point position, InputModifiers inputModifiers,
long touchPointId)
: base(device, timestamp, root, type, position, inputModifiers)
{
TouchPointId = touchPointId;
}
public long TouchPointId { get; set; }
}
}

29
src/Avalonia.Input/ScrollGestureEventArgs.cs

@ -0,0 +1,29 @@
using Avalonia.Interactivity;
namespace Avalonia.Input
{
public class ScrollGestureEventArgs : RoutedEventArgs
{
public int Id { get; }
public Vector Delta { get; }
private static int _nextId = 1;
public static int GetNextFreeId() => _nextId++;
public ScrollGestureEventArgs(int id, Vector delta) : base(Gestures.ScrollGestureEvent)
{
Id = id;
Delta = delta;
}
}
public class ScrollGestureEndedEventArgs : RoutedEventArgs
{
public int Id { get; }
public ScrollGestureEndedEventArgs(int id) : base(Gestures.ScrollGestureEndedEvent)
{
Id = id;
}
}
}

74
src/Avalonia.Input/TouchDevice.cs

@ -0,0 +1,74 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Input.Raw;
using Avalonia.VisualTree;
namespace Avalonia.Input
{
/// <summary>
/// Handles raw touch events
/// <remarks>
/// This class is supposed to be used on per-toplevel basis, don't use a shared one
/// </remarks>
/// </summary>
public class TouchDevice : IInputDevice
{
Dictionary<long, Pointer> _pointers = new Dictionary<long, Pointer>();
static InputModifiers GetModifiers(InputModifiers modifiers, bool left)
{
var mask = (InputModifiers)0x7fffffff ^ InputModifiers.LeftMouseButton ^ InputModifiers.MiddleMouseButton ^
InputModifiers.RightMouseButton;
modifiers &= mask;
if (left)
modifiers |= InputModifiers.LeftMouseButton;
return modifiers;
}
public void ProcessRawEvent(RawInputEventArgs ev)
{
var args = (RawTouchEventArgs)ev;
if (!_pointers.TryGetValue(args.TouchPointId, out var pointer))
{
if (args.Type == RawPointerEventType.TouchEnd)
return;
var hit = args.Root.InputHitTest(args.Position);
_pointers[args.TouchPointId] = pointer = new Pointer(Pointer.GetNextFreeId(),
PointerType.Touch, _pointers.Count == 0);
pointer.Capture(hit);
}
var target = pointer.Captured ?? args.Root;
if (args.Type == RawPointerEventType.TouchBegin)
{
target.RaiseEvent(new PointerPressedEventArgs(target, pointer,
args.Root, args.Position, ev.Timestamp,
new PointerPointProperties(GetModifiers(args.InputModifiers, pointer.IsPrimary)),
GetModifiers(args.InputModifiers, false)));
}
if (args.Type == RawPointerEventType.TouchEnd)
{
_pointers.Remove(args.TouchPointId);
using (pointer)
{
target.RaiseEvent(new PointerReleasedEventArgs(target, pointer,
args.Root, args.Position, ev.Timestamp,
new PointerPointProperties(GetModifiers(args.InputModifiers, false)),
GetModifiers(args.InputModifiers, pointer.IsPrimary),
pointer.IsPrimary ? MouseButton.Left : MouseButton.None));
}
}
if (args.Type == RawPointerEventType.TouchUpdate)
{
var modifiers = GetModifiers(args.InputModifiers, pointer.IsPrimary);
target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root,
args.Position, ev.Timestamp, new PointerPointProperties(modifiers), modifiers));
}
}
}
}

4
src/Avalonia.Native/Avalonia.Native.csproj

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsPackable>false</IsPackable>
@ -7,8 +7,6 @@
<CastXmlPath Condition="Exists('/usr/bin/castxml')">/usr/bin/castxml</CastXmlPath>
<CastXmlPath Condition="Exists('/usr/local/bin/castxml')">/usr/local/bin/castxml</CastXmlPath>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<!-- This is needed because Rider doesn't see generated files in obj for some reason -->
<SharpGenGeneratedCodeFolder>$(MSBuildThisFileDirectory)/Generated</SharpGenGeneratedCodeFolder>
</PropertyGroup>
<ItemGroup Condition="'$(Configuration)' == 'Release' AND '$([MSBuild]::IsOSPlatform(OSX))' == 'true'">

2
src/Avalonia.Native/WindowImplBase.cs

@ -226,7 +226,7 @@ namespace Avalonia.Native
break;
default:
Input?.Invoke(new RawMouseEventArgs(_mouse, timeStamp, _inputRoot, (RawMouseEventType)type, point.ToAvaloniaPoint(), (InputModifiers)modifiers));
Input?.Invoke(new RawPointerEventArgs(_mouse, timeStamp, _inputRoot, (RawPointerEventType)type, point.ToAvaloniaPoint(), (InputModifiers)modifiers));
break;
}
}

18
src/Avalonia.OpenGL/AngleOptions.cs

@ -0,0 +1,18 @@
using System.Collections.Generic;
namespace Avalonia.OpenGL
{
public class AngleOptions
{
public enum PlatformApi
{
DirectX9,
DirectX11
}
public List<PlatformApi> AllowedPlatformApis = new List<PlatformApi>
{
PlatformApi.DirectX9
};
}
}

71
src/Avalonia.OpenGL/EglDisplay.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Avalonia.Platform.Interop;
using static Avalonia.OpenGL.EglConsts;
@ -13,21 +14,42 @@ namespace Avalonia.OpenGL
private readonly int[] _contextAttributes;
public IntPtr Handle => _display;
private AngleOptions.PlatformApi? _angleApi;
public EglDisplay(EglInterface egl)
{
_egl = egl;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && _egl.GetPlatformDisplayEXT != null)
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
foreach (var dapi in new[] {EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE, EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE})
if (_egl.GetPlatformDisplayEXT == null)
throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl.dll");
var allowedApis = AvaloniaLocator.Current.GetService<AngleOptions>()?.AllowedPlatformApis
?? new List<AngleOptions.PlatformApi> {AngleOptions.PlatformApi.DirectX9};
foreach (var platformApi in allowedApis)
{
int dapi;
if (platformApi == AngleOptions.PlatformApi.DirectX9)
dapi = EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE;
else if (platformApi == AngleOptions.PlatformApi.DirectX11)
dapi = EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE;
else
continue;
_display = _egl.GetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE, IntPtr.Zero, new[]
{
EGL_PLATFORM_ANGLE_TYPE_ANGLE, dapi, EGL_NONE
});
if(_display != IntPtr.Zero)
if (_display != IntPtr.Zero)
{
_angleApi = platformApi;
break;
}
}
if (_display == IntPtr.Zero)
throw new OpenGlException("Unable to create ANGLE display");
}
if (_display == IntPtr.Zero)
@ -64,29 +86,35 @@ namespace Avalonia.OpenGL
if (!_egl.BindApi(cfg.Api))
continue;
var attribs = new[]
foreach(var stencilSize in new[]{8, 1, 0})
foreach (var depthSize in new []{8, 1, 0})
{
EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
EGL_RENDERABLE_TYPE, cfg.RenderableTypeBit,
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_ALPHA_SIZE, 8,
EGL_STENCIL_SIZE, 8,
EGL_DEPTH_SIZE, 8,
EGL_NONE
};
if (!_egl.ChooseConfig(_display, attribs, out _config, 1, out int numConfigs))
continue;
if (numConfigs == 0)
continue;
_contextAttributes = cfg.Attributes;
Type = cfg.Type;
var attribs = new[]
{
EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
EGL_RENDERABLE_TYPE, cfg.RenderableTypeBit,
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
EGL_ALPHA_SIZE, 8,
EGL_STENCIL_SIZE, stencilSize,
EGL_DEPTH_SIZE, depthSize,
EGL_NONE
};
if (!_egl.ChooseConfig(_display, attribs, out _config, 1, out int numConfigs))
continue;
if (numConfigs == 0)
continue;
_contextAttributes = cfg.Attributes;
Type = cfg.Type;
}
}
if (_contextAttributes == null)
throw new OpenGlException("No suitable EGL config was found");
GlInterface = GlInterface.FromNativeUtf8GetProcAddress(b => _egl.GetProcAddress(b));
}
@ -97,6 +125,7 @@ namespace Avalonia.OpenGL
public GlDisplayType Type { get; }
public GlInterface GlInterface { get; }
public EglInterface EglInterface => _egl;
public IGlContext CreateContext(IGlContext share)
{
var shareCtx = (EglContext)share;

33
src/Avalonia.OpenGL/EglGlPlatformSurface.cs

@ -26,31 +26,44 @@ namespace Avalonia.OpenGL
public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget()
{
var glSurface = _display.CreateWindowSurface(_info.Handle);
return new RenderTarget(_context, glSurface, _info);
return new RenderTarget(_display, _context, glSurface, _info);
}
class RenderTarget : IGlPlatformSurfaceRenderTarget
class RenderTarget : IGlPlatformSurfaceRenderTargetWithCorruptionInfo
{
private readonly EglDisplay _display;
private readonly EglContext _context;
private readonly EglSurface _glSurface;
private readonly IEglWindowGlPlatformSurfaceInfo _info;
private PixelSize _initialSize;
public RenderTarget(EglContext context, EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info)
public RenderTarget(EglDisplay display, EglContext context,
EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info)
{
_display = display;
_context = context;
_glSurface = glSurface;
_info = info;
_initialSize = info.Size;
}
public void Dispose() => _glSurface.Dispose();
public bool IsCorrupted => _initialSize != _info.Size;
public IGlPlatformSurfaceRenderingSession BeginDraw()
{
var l = _context.Lock();
try
{
if (IsCorrupted)
throw new RenderTargetCorruptedException();
_context.MakeCurrent(_glSurface);
return new Session(_context, _glSurface, _info, l);
_display.EglInterface.WaitClient();
_display.EglInterface.WaitGL();
_display.EglInterface.WaitNative();
return new Session(_display, _context, _glSurface, _info, l);
}
catch
{
@ -61,15 +74,19 @@ namespace Avalonia.OpenGL
class Session : IGlPlatformSurfaceRenderingSession
{
private readonly IGlContext _context;
private readonly EglContext _context;
private readonly EglSurface _glSurface;
private readonly IEglWindowGlPlatformSurfaceInfo _info;
private readonly EglDisplay _display;
private IDisposable _lock;
public Session(IGlContext context, EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info,
public Session(EglDisplay display, EglContext context,
EglSurface glSurface, IEglWindowGlPlatformSurfaceInfo info,
IDisposable @lock)
{
_context = context;
_display = display;
_glSurface = glSurface;
_info = info;
_lock = @lock;
@ -78,7 +95,11 @@ namespace Avalonia.OpenGL
public void Dispose()
{
_context.Display.GlInterface.Flush();
_display.EglInterface.WaitGL();
_glSurface.SwapBuffers();
_display.EglInterface.WaitClient();
_display.EglInterface.WaitGL();
_display.EglInterface.WaitNative();
_context.Display.ClearContext();
_lock.Dispose();
}

43
src/Avalonia.OpenGL/EglInterface.cs

@ -1,4 +1,5 @@
using System;
using System.Runtime.InteropServices;
using Avalonia.Platform;
using Avalonia.Platform.Interop;
@ -15,13 +16,28 @@ namespace Avalonia.OpenGL
{
}
[DllImport("libegl.dll", CharSet = CharSet.Ansi)]
static extern IntPtr eglGetProcAddress(string proc);
static Func<string, bool, IntPtr> Load()
{
var os = AvaloniaLocator.Current.GetService<IRuntimePlatform>().GetRuntimeInfo().OperatingSystem;
if(os == OperatingSystemType.Linux || os == OperatingSystemType.Android)
return Load("libEGL.so.1");
if (os == OperatingSystemType.WinNT)
return Load(@"libegl.dll");
{
var disp = eglGetProcAddress("eglGetPlatformDisplayEXT");
if (disp == IntPtr.Zero)
throw new OpenGlException("libegl.dll doesn't have eglGetPlatformDisplayEXT entry point");
return (name, optional) =>
{
var r = eglGetProcAddress(name);
if (r == IntPtr.Zero && !optional)
throw new OpenGlException($"Entry point {r} is not found");
return r;
};
}
throw new PlatformNotSupportedException();
}
@ -91,6 +107,31 @@ namespace Avalonia.OpenGL
[GlEntryPoint("eglGetConfigAttrib")]
public EglGetConfigAttrib GetConfigAttrib { get; }
public delegate bool EglWaitGL();
[GlEntryPoint("eglWaitGL")]
public EglWaitGL WaitGL { get; }
public delegate bool EglWaitClient();
[GlEntryPoint("eglWaitClient")]
public EglWaitGL WaitClient { get; }
public delegate bool EglWaitNative();
[GlEntryPoint("eglWaitNative")]
public EglWaitGL WaitNative { get; }
public delegate IntPtr EglQueryString(IntPtr display, int i);
[GlEntryPoint("eglQueryString")]
public EglQueryString QueryStringNative { get; }
public string QueryString(IntPtr display, int i)
{
var rv = QueryStringNative(display, i);
if (rv == IntPtr.Zero)
return null;
return Marshal.PtrToStringAnsi(rv);
}
// ReSharper restore UnassignedGetOnlyAutoProperty
}
}

7
src/Avalonia.OpenGL/IGlPlatformSurfaceRenderTarget.cs

@ -6,4 +6,9 @@ namespace Avalonia.OpenGL
{
IGlPlatformSurfaceRenderingSession BeginDraw();
}
}
public interface IGlPlatformSurfaceRenderTargetWithCorruptionInfo : IGlPlatformSurfaceRenderTarget
{
bool IsCorrupted { get; }
}
}

5
src/Avalonia.ReactiveUI/AppBuilderExtensions.cs

@ -21,9 +21,8 @@ namespace Avalonia.ReactiveUI
return builder.AfterSetup(_ =>
{
RxApp.MainThreadScheduler = AvaloniaScheduler.Instance;
Locator.CurrentMutable.Register(
() => new AvaloniaActivationForViewFetcher(),
typeof(IActivationForViewFetcher));
Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher));
Locator.CurrentMutable.RegisterConstant(new AutoDataTemplateBindingHook(), typeof(IPropertyBindingHook));
});
}
}

55
src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs

@ -0,0 +1,55 @@
using System;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Layout;
using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.Templates;
using ReactiveUI;
namespace Avalonia.ReactiveUI
{
/// <summary>
/// AutoDataTemplateBindingHook is a binding hook that checks ItemsControls
/// that don't have DataTemplates, and assigns a default DataTemplate that
/// loads the View associated with each ViewModel.
/// </summary>
public class AutoDataTemplateBindingHook : IPropertyBindingHook
{
private static FuncDataTemplate DefaultItemTemplate = new FuncDataTemplate<object>(x =>
{
var control = new ViewModelViewHost();
var context = control.GetObservable(Control.DataContextProperty);
control.Bind(ViewModelViewHost.ViewModelProperty, context);
control.HorizontalContentAlignment = HorizontalAlignment.Stretch;
control.VerticalContentAlignment = VerticalAlignment.Stretch;
return control;
},
true);
/// <inheritdoc/>
public bool ExecuteHook(
object source, object target,
Func<IObservedChange<object, object>[]> getCurrentViewModelProperties,
Func<IObservedChange<object, object>[]> getCurrentViewProperties,
BindingDirection direction)
{
var viewProperties = getCurrentViewProperties();
var lastViewProperty = viewProperties.LastOrDefault();
var itemsControl = lastViewProperty?.Sender as ItemsControl;
if (itemsControl == null)
return true;
var propertyName = viewProperties.Last().GetPropertyName();
if (propertyName != "Items" &&
propertyName != "ItemsSource")
return true;
if (itemsControl.ItemTemplate != null)
return true;
itemsControl.ItemTemplate = DefaultItemTemplate;
return true;
}
}
}

1
src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj

@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<PackageId>Avalonia.ReactiveUI</PackageId>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\packages\Avalonia\Avalonia.csproj" />

143
src/Avalonia.ReactiveUI/RoutedViewHost.cs

@ -53,33 +53,13 @@ namespace Avalonia.ReactiveUI
/// ReactiveUI routing documentation website</see> for more info.
/// </para>
/// </remarks>
public class RoutedViewHost : UserControl, IActivatable, IEnableLogger
public class RoutedViewHost : TransitioningContentControl, IActivatable, IEnableLogger
{
/// <summary>
/// The router dependency property.
/// <see cref="AvaloniaProperty"/> for the <see cref="Router"/> property.
/// </summary>
public static readonly AvaloniaProperty<RoutingState> RouterProperty =
AvaloniaProperty.Register<RoutedViewHost, RoutingState>(nameof(Router));
/// <summary>
/// The default content property.
/// </summary>
public static readonly AvaloniaProperty<object> DefaultContentProperty =
AvaloniaProperty.Register<RoutedViewHost, object>(nameof(DefaultContent));
/// <summary>
/// Fade in animation property.
/// </summary>
public static readonly AvaloniaProperty<IAnimation> FadeInAnimationProperty =
AvaloniaProperty.Register<RoutedViewHost, IAnimation>(nameof(DefaultContent),
CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25)));
/// <summary>
/// Fade out animation property.
/// </summary>
public static readonly AvaloniaProperty<IAnimation> FadeOutAnimationProperty =
AvaloniaProperty.Register<RoutedViewHost, IAnimation>(nameof(DefaultContent),
CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25)));
/// <summary>
/// Initializes a new instance of the <see cref="RoutedViewHost"/> class.
@ -104,42 +84,6 @@ namespace Avalonia.ReactiveUI
set => SetValue(RouterProperty, value);
}
/// <summary>
/// Gets or sets the content displayed whenever there is no page currently routed.
/// </summary>
public object DefaultContent
{
get => GetValue(DefaultContentProperty);
set => SetValue(DefaultContentProperty, value);
}
/// <summary>
/// Gets or sets the animation played when page appears.
/// </summary>
public IAnimation FadeInAnimation
{
get => GetValue(FadeInAnimationProperty);
set => SetValue(FadeInAnimationProperty, value);
}
/// <summary>
/// Gets or sets the animation played when page disappears.
/// </summary>
public IAnimation FadeOutAnimation
{
get => GetValue(FadeOutAnimationProperty);
set => SetValue(FadeOutAnimationProperty, value);
}
/// <summary>
/// Duplicates the Content property with a private setter.
/// </summary>
public new object Content
{
get => base.Content;
private set => base.Content = value;
}
/// <summary>
/// Gets or sets the ReactiveUI view locator used by this router.
/// </summary>
@ -149,82 +93,29 @@ namespace Avalonia.ReactiveUI
/// Invoked when ReactiveUI router navigates to a view model.
/// </summary>
/// <param name="viewModel">ViewModel to which the user navigates.</param>
/// <exception cref="Exception">
/// Thrown when ViewLocator is unable to find the appropriate view.
/// </exception>
private void NavigateToViewModel(IRoutableViewModel viewModel)
private void NavigateToViewModel(object viewModel)
{
if (viewModel == null)
{
this.Log().Info("ViewModel is null, falling back to default content.");
UpdateContent(DefaultContent);
this.Log().Info("ViewModel is null. Falling back to default content.");
Content = DefaultContent;
return;
}
var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current;
var view = viewLocator.ResolveView(viewModel);
if (view == null) throw new Exception($"Couldn't find view for '{viewModel}'. Is it registered?");
var viewInstance = viewLocator.ResolveView(viewModel);
if (viewInstance == null)
{
this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content.");
Content = DefaultContent;
return;
}
this.Log().Info($"Ready to show {view} with autowired {viewModel}.");
view.ViewModel = viewModel;
if (view is IStyledElement styled)
this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}.");
viewInstance.ViewModel = viewModel;
if (viewInstance is IStyledElement styled)
styled.DataContext = viewModel;
UpdateContent(view);
}
/// <summary>
/// Updates the content with transitions.
/// </summary>
/// <param name="newContent">New content to set.</param>
private async void UpdateContent(object newContent)
{
if (FadeOutAnimation != null)
await FadeOutAnimation.RunAsync(this, Clock);
Content = newContent;
if (FadeInAnimation != null)
await FadeInAnimation.RunAsync(this, Clock);
}
/// <summary>
/// Creates opacity animation for this routed view host.
/// </summary>
/// <param name="from">Opacity to start from.</param>
/// <param name="to">Opacity to finish with.</param>
/// <param name="duration">Duration of the animation.</param>
/// <returns>Animation object instance.</returns>
private static IAnimation CreateOpacityAnimation(double from, double to, TimeSpan duration)
{
return new Avalonia.Animation.Animation
{
Duration = duration,
Children =
{
new KeyFrame
{
Setters =
{
new Setter
{
Property = OpacityProperty,
Value = from
}
},
Cue = new Cue(0d)
},
new KeyFrame
{
Setters =
{
new Setter
{
Property = OpacityProperty,
Value = to
}
},
Cue = new Cue(1d)
}
}
};
Content = viewInstance;
}
}
}
}

75
src/Avalonia.ReactiveUI/TransitioningContentControl.cs

@ -0,0 +1,75 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Styling;
namespace Avalonia.ReactiveUI
{
/// <summary>
/// A ContentControl that animates the transition when its content is changed.
/// </summary>
public class TransitioningContentControl : ContentControl, IStyleable
{
/// <summary>
/// <see cref="AvaloniaProperty"/> for the <see cref="PageTransition"/> property.
/// </summary>
public static readonly AvaloniaProperty<IPageTransition> PageTransitionProperty =
AvaloniaProperty.Register<TransitioningContentControl, IPageTransition>(nameof(PageTransition),
new CrossFade(TimeSpan.FromSeconds(0.5)));
/// <summary>
/// <see cref="AvaloniaProperty"/> for the <see cref="DefaultContent"/> property.
/// </summary>
public static readonly AvaloniaProperty<object> DefaultContentProperty =
AvaloniaProperty.Register<TransitioningContentControl, object>(nameof(DefaultContent));
/// <summary>
/// Gets or sets the animation played when content appears and disappears.
/// </summary>
public IPageTransition PageTransition
{
get => GetValue(PageTransitionProperty);
set => SetValue(PageTransitionProperty, value);
}
/// <summary>
/// Gets or sets the content displayed whenever there is no page currently routed.
/// </summary>
public object DefaultContent
{
get => GetValue(DefaultContentProperty);
set => SetValue(DefaultContentProperty, value);
}
/// <summary>
/// Gets or sets the content with animation.
/// </summary>
public new object Content
{
get => base.Content;
set => UpdateContentWithTransition(value);
}
/// <summary>
/// TransitioningContentControl uses the default ContentControl
/// template from Avalonia default theme.
/// </summary>
Type IStyleable.StyleKey => typeof(ContentControl);
/// <summary>
/// Updates the content with transitions.
/// </summary>
/// <param name="content">New content to set.</param>
private async void UpdateContentWithTransition(object content)
{
if (PageTransition != null)
await PageTransition.Start(this, null, true);
base.Content = content;
if (PageTransition != null)
await PageTransition.Start(null, this, true);
}
}
}

80
src/Avalonia.ReactiveUI/ViewModelViewHost.cs

@ -0,0 +1,80 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive.Disposables;
using ReactiveUI;
using Splat;
namespace Avalonia.ReactiveUI
{
/// <summary>
/// This content control will automatically load the View associated with
/// the ViewModel property and display it. This control is very useful
/// inside a DataTemplate to display the View associated with a ViewModel.
/// </summary>
public class ViewModelViewHost : TransitioningContentControl, IViewFor, IEnableLogger
{
/// <summary>
/// <see cref="AvaloniaProperty"/> for the <see cref="ViewModel"/> property.
/// </summary>
public static readonly AvaloniaProperty<object> ViewModelProperty =
AvaloniaProperty.Register<ViewModelViewHost, object>(nameof(ViewModel));
/// <summary>
/// Initializes a new instance of the <see cref="ViewModelViewHost"/> class.
/// </summary>
public ViewModelViewHost()
{
this.WhenActivated(disposables =>
{
this.WhenAnyValue(x => x.ViewModel)
.Subscribe(NavigateToViewModel)
.DisposeWith(disposables);
});
}
/// <summary>
/// Gets or sets the ViewModel to display.
/// </summary>
public object ViewModel
{
get => GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value);
}
/// <summary>
/// Gets or sets the view locator.
/// </summary>
public IViewLocator ViewLocator { get; set; }
/// <summary>
/// Invoked when ReactiveUI router navigates to a view model.
/// </summary>
/// <param name="viewModel">ViewModel to which the user navigates.</param>
private void NavigateToViewModel(object viewModel)
{
if (viewModel == null)
{
this.Log().Info("ViewModel is null. Falling back to default content.");
Content = DefaultContent;
return;
}
var viewLocator = ViewLocator ?? global::ReactiveUI.ViewLocator.Current;
var viewInstance = viewLocator.ResolveView(viewModel);
if (viewInstance == null)
{
this.Log().Warn($"Couldn't find view for '{viewModel}'. Is it registered? Falling back to default content.");
Content = DefaultContent;
return;
}
this.Log().Info($"Ready to show {viewInstance} with autowired {viewModel}.");
viewInstance.ViewModel = viewModel;
if (viewInstance is IStyledElement styled)
styled.DataContext = viewModel;
Content = viewInstance;
}
}
}

2
src/Avalonia.Styling/Controls/IResourceProvider.cs

@ -28,6 +28,6 @@ namespace Avalonia.Controls
/// <returns>
/// True if the resource if found, otherwise false.
/// </returns>
bool TryGetResource(string key, out object value);
bool TryGetResource(object key, out object value);
}
}

2
src/Avalonia.Styling/Controls/ResourceDictionary.cs

@ -69,7 +69,7 @@ namespace Avalonia.Controls
}
/// <inheritdoc/>
public bool TryGetResource(string key, out object value)
public bool TryGetResource(object key, out object value)
{
if (TryGetValue(key, out value))
{

10
src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs

@ -11,7 +11,7 @@ namespace Avalonia.Controls
/// <param name="control">The control.</param>
/// <param name="key">The resource key.</param>
/// <returns>The resource, or <see cref="AvaloniaProperty.UnsetValue"/> if not found.</returns>
public static object FindResource(this IResourceNode control, string key)
public static object FindResource(this IResourceNode control, object key)
{
if (control.TryFindResource(key, out var value))
{
@ -28,7 +28,7 @@ namespace Avalonia.Controls
/// <param name="key">The resource key.</param>
/// <param name="value">On return, contains the resource if found, otherwise null.</param>
/// <returns>True if the resource was found; otherwise false.</returns>
public static bool TryFindResource(this IResourceNode control, string key, out object value)
public static bool TryFindResource(this IResourceNode control, object key, out object value)
{
Contract.Requires<ArgumentNullException>(control != null);
Contract.Requires<ArgumentNullException>(key != null);
@ -52,7 +52,7 @@ namespace Avalonia.Controls
return false;
}
public static IObservable<object> GetResourceObservable(this IResourceNode target, string key)
public static IObservable<object> GetResourceObservable(this IResourceNode target, object key)
{
return new ResourceObservable(target, key);
}
@ -60,9 +60,9 @@ namespace Avalonia.Controls
private class ResourceObservable : LightweightObservableBase<object>
{
private readonly IResourceNode _target;
private readonly string _key;
private readonly object _key;
public ResourceObservable(IResourceNode target, string key)
public ResourceObservable(IResourceNode target, object key)
{
_target = target;
_key = key;

19
src/Avalonia.Styling/StyledElement.cs

@ -415,7 +415,7 @@ namespace Avalonia
}
/// <inheritdoc/>
bool IResourceProvider.TryGetResource(string key, out object value)
bool IResourceProvider.TryGetResource(object key, out object value)
{
value = null;
return (_resources?.TryGetResource(key, out value) ?? false) ||
@ -677,23 +677,6 @@ namespace Avalonia
if (Name != null)
{
_nameScope?.Register(Name, this);
var visualParent = Parent as StyledElement;
if (this is INameScope && visualParent != null)
{
// If we have e.g. a named UserControl in a window then we want that control
// to be findable by name from the Window, so register with both name scopes.
// This differs from WPF's behavior in that XAML manually registers controls
// with name scopes based on the XAML file in which the name attribute appears,
// but we're trying to avoid XAML magic in Avalonia in order to made code-
// created UIs easy. This will cause problems if a UserControl declares a name
// in its XAML and that control is included multiple times in a parent control
// (as the name will be duplicated), however at the moment I'm fine with saying
// "don't do that".
var parentNameScope = NameScope.FindNameScope(visualParent);
parentNameScope?.Register(Name, this);
}
}
}

2
src/Avalonia.Styling/Styling/Style.cs

@ -171,7 +171,7 @@ namespace Avalonia.Styling
}
/// <inheritdoc/>
public bool TryGetResource(string key, out object result)
public bool TryGetResource(object key, out object result)
{
result = null;
return _resources?.TryGetResource(key, out result) ?? false;

2
src/Avalonia.Styling/Styling/Styles.cs

@ -178,7 +178,7 @@ namespace Avalonia.Styling
}
/// <inheritdoc/>
public bool TryGetResource(string key, out object value)
public bool TryGetResource(object key, out object value)
{
if (_resources != null && _resources.TryGetValue(key, out value))
{

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save