Browse Source

Merge branch 'master' into refactor/2709-mutable-pen

pull/2744/head
Steven Kirk 7 years ago
committed by GitHub
parent
commit
3b2d34b7af
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      azure-pipelines.yml
  2. 2
      build/SkiaSharp.props
  3. 37
      nukebuild/Build.cs
  4. 3
      readme.md
  5. 2
      samples/ControlCatalog.Android/ControlCatalog.Android.csproj
  6. 21
      samples/ControlCatalog.NetCore/Program.cs
  7. 1
      samples/ControlCatalog/MainView.xaml
  8. 4
      samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml
  9. 33
      samples/ControlCatalog/Pages/TabStripPage.xaml
  10. 45
      samples/ControlCatalog/Pages/TabStripPage.xaml.cs
  11. 3
      samples/ControlCatalog/SideBar.xaml
  12. 5
      samples/RenderDemo/SideBar.xaml
  13. 2
      src/Android/Avalonia.Android/Avalonia.Android.csproj
  14. 2
      src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj
  15. 72
      src/Avalonia.Base/AvaloniaObject.cs
  16. 2
      src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs
  17. 9
      src/Avalonia.Base/IPriorityValueOwner.cs
  18. 34
      src/Avalonia.Base/PriorityValue.cs
  19. 150
      src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs
  20. 206
      src/Avalonia.Base/Utilities/DeferredSetter.cs
  21. 152
      src/Avalonia.Base/ValueStore.cs
  22. 3
      src/Avalonia.Controls.DataGrid/Themes/Default.xaml
  23. 26
      src/Avalonia.Controls/AutoCompleteBox.cs
  24. 3
      src/Avalonia.Controls/ComboBox.cs
  25. 39
      src/Avalonia.Controls/ContextMenu.cs
  26. 12
      src/Avalonia.Controls/Generators/IItemContainerGenerator.cs
  27. 17
      src/Avalonia.Controls/Generators/ItemContainerGenerator.cs
  28. 14
      src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs
  29. 5
      src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
  30. 15
      src/Avalonia.Controls/ItemsControl.cs
  31. 2
      src/Avalonia.Controls/LayoutTransformControl.cs
  32. 8
      src/Avalonia.Controls/ListBox.cs
  33. 46
      src/Avalonia.Controls/Menu.cs
  34. 5
      src/Avalonia.Controls/MenuBase.cs
  35. 85
      src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs
  36. 2
      src/Avalonia.Controls/Presenters/CarouselPresenter.cs
  37. 2
      src/Avalonia.Controls/Presenters/ItemContainerSync.cs
  38. 2
      src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs
  39. 9
      src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
  40. 15
      src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
  41. 35
      src/Avalonia.Controls/Primitives/Popup.cs
  42. 54
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  43. 9
      src/Avalonia.Controls/Primitives/TabStrip.cs
  44. 35
      src/Avalonia.Controls/Templates/FuncMemberSelector.cs
  45. 18
      src/Avalonia.Controls/Templates/IMemberSelector.cs
  46. 105
      src/Avalonia.Controls/TreeView.cs
  47. 2
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
  48. 37
      src/Avalonia.Diagnostics/DevTools.xaml
  49. 8
      src/Avalonia.Diagnostics/DevTools.xaml.cs
  50. 2
      src/Avalonia.Diagnostics/ViewLocator.cs
  51. 65
      src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs
  52. 13
      src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs
  53. 16
      src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs
  54. 7
      src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs
  55. 16
      src/Avalonia.Diagnostics/Views/ControlDetailsView.cs
  56. 5
      src/Avalonia.Diagnostics/Views/PropertyChangedExtensions.cs
  57. 3
      src/Avalonia.Diagnostics/Views/TreePage.xaml.cs
  58. 3
      src/Avalonia.Input/Raw/RawPointerEventArgs.cs
  59. 8
      src/Avalonia.Input/TouchDevice.cs
  60. 8
      src/Avalonia.OpenGL/EglContext.cs
  61. 94
      src/Avalonia.OpenGL/EglDisplay.cs
  62. 5
      src/Avalonia.OpenGL/EglInterface.cs
  63. 2
      src/Avalonia.OpenGL/GlInterface.cs
  64. 6
      src/Avalonia.Styling/Controls/NameScopeLocator.cs
  65. 1
      src/Avalonia.Themes.Default/AutoCompleteBox.xaml
  66. 3
      src/Avalonia.Themes.Default/Carousel.xaml
  67. 1
      src/Avalonia.Themes.Default/ComboBox.xaml
  68. 2
      src/Avalonia.Themes.Default/DataValidationErrors.xaml
  69. 5
      src/Avalonia.Themes.Default/ItemsControl.xaml
  70. 3
      src/Avalonia.Themes.Default/ListBox.xaml
  71. 6
      src/Avalonia.Themes.Default/MenuItem.xaml
  72. 3
      src/Avalonia.Themes.Default/TabControl.xaml
  73. 3
      src/Avalonia.Themes.Default/TabStrip.xaml
  74. 3
      src/Avalonia.Themes.Default/TreeView.xaml
  75. 3
      src/Avalonia.Themes.Default/TreeViewItem.xaml
  76. 5
      src/Avalonia.Visuals/Rendering/RenderLayers.cs
  77. 88
      src/Linux/Avalonia.LinuxFramebuffer/EvDevDevice.cs
  78. 38
      src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs
  79. 12
      src/Linux/Avalonia.LinuxFramebuffer/Input/IInputBackend.cs
  80. 7
      src/Linux/Avalonia.LinuxFramebuffer/Input/IScreenInfoProvider.cs
  81. 183
      src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs
  82. 139
      src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputNativeUnsafeMethods.cs
  83. 46
      src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
  84. 117
      src/Linux/Avalonia.LinuxFramebuffer/Mice.cs
  85. 13
      src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs
  86. 292
      src/Linux/Avalonia.LinuxFramebuffer/Output/Drm.cs
  87. 158
      src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs
  88. 250
      src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs
  89. 11
      src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs
  90. 7
      src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs
  91. 2
      src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
  92. 24
      src/Markup/Avalonia.Markup.Xaml/Converters/MemberSelectorTypeConverter.cs
  93. 48
      src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs
  94. 2
      src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs
  95. 78
      tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs
  96. 2
      tests/Avalonia.Base.UnitTests/PriorityValueTests.cs
  97. 2
      tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
  98. 65
      tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs
  99. 3
      tests/Avalonia.Benchmarks/Base/Properties.cs
  100. 54
      tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs

2
azure-pipelines.yml

@ -102,7 +102,7 @@ jobs:
- job: Windows
pool:
vmImage: 'vs2017-win2016'
vmImage: 'windows-2019'
steps:
- task: CmdLine@2
displayName: 'Install Nuke'

2
build/SkiaSharp.props

@ -1,6 +1,6 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<PackageReference Include="SkiaSharp" Version="1.68.0" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="Avalonia.Skia.Linux.Natives" Version="1.68.0.1" />
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="Avalonia.Skia.Linux.Natives" Version="1.68.0.2" />
</ItemGroup>
</Project>

37
nukebuild/Build.cs

@ -89,6 +89,29 @@ partial class Build : NukeBuild
}
IReadOnlyCollection<Output> MsBuildCommon(
string projectFile,
Configure<MSBuildSettings> configurator = null)
{
return MSBuild(projectFile, c =>
{
// This is required for VS2019 image on Azure Pipelines
if (Parameters.IsRunningOnWindows && Parameters.IsRunningOnAzure)
{
var javaSdk = Environment.GetEnvironmentVariable("JAVA_HOME_8_X64");
if (javaSdk != null)
c = c.AddProperty("JavaSdkDirectory", javaSdk);
}
c = c.AddProperty("PackageVersion", Parameters.Version)
.AddProperty("iOSRoslynPathHackRequired", "true")
.SetToolPath(MsBuildExe.Value)
.SetConfiguration(Parameters.Configuration)
.SetVerbosity(MSBuildVerbosity.Minimal);
c = configurator?.Invoke(c) ?? c;
return c;
});
}
Target Clean => _ => _.Executes(() =>
{
DeleteDirectories(Parameters.BuildDirs);
@ -105,13 +128,8 @@ partial class Build : NukeBuild
.Executes(() =>
{
if (Parameters.IsRunningOnWindows)
MSBuild(Parameters.MSBuildSolution, c => c
MsBuildCommon(Parameters.MSBuildSolution, c => c
.SetArgumentConfigurator(a => a.Add("/r"))
.SetConfiguration(Parameters.Configuration)
.SetVerbosity(MSBuildVerbosity.Minimal)
.AddProperty("PackageVersion", Parameters.Version)
.AddProperty("iOSRoslynPathHackRequired", "true")
.SetToolPath(MsBuildExe.Value)
.AddTargets("Build")
);
@ -237,12 +255,7 @@ partial class Build : NukeBuild
{
if (Parameters.IsRunningOnWindows)
MSBuild(Parameters.MSBuildSolution, c => c
.SetConfiguration(Parameters.Configuration)
.SetVerbosity(MSBuildVerbosity.Minimal)
.AddProperty("PackageVersion", Parameters.Version)
.AddProperty("iOSRoslynPathHackRequired", "true")
.SetToolPath(MsBuildExe.Value)
MsBuildCommon(Parameters.MSBuildSolution, c => c
.AddTargets("Pack"));
else
DotNetPack(Parameters.MSBuildSolution, c =>

3
readme.md

@ -32,9 +32,6 @@ Install-Package Avalonia.Desktop
## Bleeding Edge Builds
Try out the latest build of Avalonia available for download here:
https://ci.appveyor.com/project/AvaloniaUI/Avalonia/branch/master/artifacts
or use nightly build feeds as described here:
https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed

2
samples/ControlCatalog.Android/ControlCatalog.Android.csproj

@ -16,7 +16,7 @@
<AndroidResgenFile>Resources\Resource.Designer.cs</AndroidResgenFile>
<GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
<AndroidUseLatestPlatformSdk>False</AndroidUseLatestPlatformSdk>
<TargetFrameworkVersion>v4.4</TargetFrameworkVersion>
<TargetFrameworkVersion>v8.0</TargetFrameworkVersion>
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">

21
samples/ControlCatalog.NetCore/Program.cs

@ -4,6 +4,7 @@ using System.Linq;
using System.Threading;
using Avalonia;
using Avalonia.Controls;
using Avalonia.LinuxFramebuffer.Output;
using Avalonia.Skia;
using Avalonia.ReactiveUI;
@ -29,8 +30,13 @@ namespace ControlCatalog.NetCore
var builder = BuildAvaloniaApp();
if (args.Contains("--fbdev"))
{
System.Threading.ThreadPool.QueueUserWorkItem(_ => ConsoleSilencer());
return builder.StartLinuxFramebuffer(args);
SilenceConsole();
return builder.StartLinuxFbDev(args);
}
else if (args.Contains("--drm"))
{
SilenceConsole();
return builder.StartLinuxDrm(args);
}
else
return builder.StartWithClassicDesktopLifetime(args);
@ -51,11 +57,14 @@ namespace ControlCatalog.NetCore
.UseSkia()
.UseReactiveUI();
static void ConsoleSilencer()
static void SilenceConsole()
{
Console.CursorVisible = false;
while (true)
Console.ReadKey(true);
new Thread(() =>
{
Console.CursorVisible = false;
while (true)
Console.ReadKey(true);
}) {IsBackground = true}.Start();
}
}
}

1
samples/ControlCatalog/MainView.xaml

@ -36,6 +36,7 @@
<TabItem Header="RadioButton"><pages:RadioButtonPage/></TabItem>
<TabItem Header="Slider"><pages:SliderPage/></TabItem>
<TabItem Header="TabControl"><pages:TabControlPage/></TabItem>
<TabItem Header="TabStrip"><pages:TabStripPage/></TabItem>
<TabItem Header="TextBox"><pages:TextBoxPage/></TabItem>
<TabItem Header="ToolTip"><pages:ToolTipPage/></TabItem>
<TabItem Header="TreeView"><pages:TreeViewPage/></TabItem>

4
samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml

@ -37,10 +37,6 @@
<StackPanel Orientation="Vertical">
<TextBlock Text="ValueMemeberSelector"/>
<AutoCompleteBox Width="200"
Margin="0,0,0,8"
ValueMemberSelector="Capital"/>
<TextBlock Text="ValueMemberBinding"/>
<AutoCompleteBox Width="200"
Margin="0,0,0,8"

33
samples/ControlCatalog/Pages/TabStripPage.xaml

@ -0,0 +1,33 @@
<UserControl xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.TabStripPage"
xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Spacing="4">
<TextBlock Classes="h1">TabStrip</TextBlock>
<TextBlock Classes="h2">A control which displays a selectable strip of tabs</TextBlock>
<Separator Margin="0 16"/>
<TextBlock Classes="h1">Defined in XAML</TextBlock>
<TabStrip>
<TabStripItem>Item 1</TabStripItem>
<TabStripItem>Item 2</TabStripItem>
<TabStripItem IsEnabled="False">Disabled</TabStripItem>
</TabStrip>
<Separator Margin="0 16"/>
<TextBlock Classes="h1">Dynamically generated</TextBlock>
<TabStrip Items="{Binding}">
<TabStrip.Styles>
<Style Selector="TabStripItem">
<Setter Property="IsEnabled" Value="{Binding IsEnabled}"/>
</Style>
</TabStrip.Styles>
<TabStrip.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Header}"/>
</DataTemplate>
</TabStrip.ItemTemplate>
</TabStrip>
</StackPanel>
</UserControl>

45
samples/ControlCatalog/Pages/TabStripPage.xaml.cs

@ -0,0 +1,45 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
namespace ControlCatalog.Pages
{
public class TabStripPage : UserControl
{
public TabStripPage()
{
InitializeComponent();
DataContext = new[]
{
new TabStripItemViewModel
{
Header = "Item 1",
},
new TabStripItemViewModel
{
Header = "Item 2",
},
new TabStripItemViewModel
{
Header = "Disabled",
IsEnabled = false,
},
};
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private class TabStripItemViewModel
{
public string Header { get; set; }
public bool IsEnabled { get; set; } = true;
}
}
}

3
samples/ControlCatalog/SideBar.xaml

@ -29,8 +29,7 @@
Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
MemberSelector="{TemplateBinding MemberSelector}">
ItemTemplate="{TemplateBinding ItemTemplate}">
</ItemsPresenter>
</ScrollViewer>
<ContentPresenter

5
samples/RenderDemo/SideBar.xaml

@ -20,8 +20,7 @@
Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
MemberSelector="{TemplateBinding MemberSelector}">
ItemTemplate="{TemplateBinding ItemTemplate}">
</ItemsPresenter>
</ScrollViewer>
<ContentPresenter
@ -63,4 +62,4 @@
<Style Selector="TabControl.sidebar > TabItem:selected /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush2}"/>
</Style>
</Styles>
</Styles>

2
src/Android/Avalonia.Android/Avalonia.Android.csproj

@ -1,6 +1,6 @@
<Project Sdk="MSBuild.Sdk.Extras">
<PropertyGroup>
<TargetFramework>monoandroid44</TargetFramework>
<TargetFramework>monoandroid80</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>

2
src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj

@ -16,7 +16,7 @@
<AndroidResgenFile>Resources\Resource.Designer.cs</AndroidResgenFile>
<GenerateSerializationAssemblies>Off</GenerateSerializationAssemblies>
<AndroidUseLatestPlatformSdk>False</AndroidUseLatestPlatformSdk>
<TargetFrameworkVersion>v4.4</TargetFrameworkVersion>
<TargetFrameworkVersion>v8.0</TargetFrameworkVersion>
<AndroidManifest>Properties\AndroidManifest.xml</AndroidManifest>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">

72
src/Avalonia.Base/AvaloniaObject.cs

@ -466,7 +466,7 @@ namespace Avalonia
/// <param name="oldValue">The old property value.</param>
/// <param name="newValue">The new property value.</param>
/// <param name="priority">The priority of the binding that produced the value.</param>
protected void RaisePropertyChanged(
protected internal void RaisePropertyChanged(
AvaloniaProperty property,
object oldValue,
object newValue,
@ -508,45 +508,6 @@ namespace Avalonia
}
}
/// <summary>
/// A callback type for encapsulating complex logic for setting direct properties.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="value">The value to which to set the property.</param>
/// <param name="field">The backing field for the property.</param>
/// <param name="notifyWrapper">A wrapper for the property-changed notification.</param>
protected delegate void SetAndRaiseCallback<T>(T value, ref T field, Action<Action> notifyWrapper);
/// <summary>
/// Sets the backing field for a direct avalonia property, raising the
/// <see cref="PropertyChanged"/> event if the value has changed.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <param name="field">The backing field.</param>
/// <param name="setterCallback">A callback called to actually set the value to the backing field.</param>
/// <param name="value">The value.</param>
/// <returns>
/// True if the value changed, otherwise false.
/// </returns>
protected bool SetAndRaise<T>(
AvaloniaProperty<T> property,
ref T field,
SetAndRaiseCallback<T> setterCallback,
T value)
{
Contract.Requires<ArgumentNullException>(setterCallback != null);
return Values.Setter.SetAndNotify(
property,
ref field,
(object update, ref T backing, Action<Action> notify) =>
{
setterCallback((T)update, ref backing, notify);
return true;
},
value);
}
/// <summary>
/// Sets the backing field for a direct avalonia property, raising the
/// <see cref="PropertyChanged"/> event if the value has changed.
@ -561,32 +522,15 @@ namespace Avalonia
protected bool SetAndRaise<T>(AvaloniaProperty<T> property, ref T field, T value)
{
VerifyAccess();
return SetAndRaise(
property,
ref field,
(T val, ref T backing, Action<Action> notifyWrapper)
=> SetAndRaiseCore(property, ref backing, val, notifyWrapper),
value);
}
/// <summary>
/// Default assignment logic for SetAndRaise.
/// </summary>
/// <typeparam name="T">The type of the property.</typeparam>
/// <param name="property">The property.</param>
/// <param name="field">The backing field.</param>
/// <param name="value">The value.</param>
/// <param name="notifyWrapper">A wrapper for the property-changed notification.</param>
/// <returns>
/// True if the value changed, otherwise false.
/// </returns>
private bool SetAndRaiseCore<T>(AvaloniaProperty property, ref T field, T value, Action<Action> notifyWrapper)
{
var old = field;
field = value;
if (EqualityComparer<T>.Default.Equals(field, value))
{
return false;
}
DeferredSetter<T> setter = Values.GetDirectDeferredSetter(property);
notifyWrapper(() => RaisePropertyChanged(property, old, value, BindingPriority.LocalValue));
return true;
return setter.SetAndNotify(this, property, ref field, value);
}
/// <summary>

2
src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs

@ -31,7 +31,7 @@ namespace Avalonia.Data.Converters
{
if (value == null)
{
return AvaloniaProperty.UnsetValue;
return targetType.IsValueType ? AvaloniaProperty.UnsetValue : null;
}
if (typeof(ICommand).IsAssignableFrom(targetType) && value is Delegate d && d.Method.GetParameters().Length <= 1)

9
src/Avalonia.Base/IPriorityValueOwner.cs

@ -29,6 +29,13 @@ namespace Avalonia
/// <param name="notification">The notification.</param>
void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification);
/// <summary>
/// Returns deferred setter for given non-direct property.
/// </summary>
/// <param name="property">Property.</param>
/// <returns>Deferred setter for given property.</returns>
DeferredSetter<object> GetNonDirectDeferredSetter(AvaloniaProperty property);
/// <summary>
/// Logs a binding error.
/// </summary>
@ -40,7 +47,5 @@ namespace Avalonia
/// Ensures that the current thread is the UI thread.
/// </summary>
void VerifyAccess();
DeferredSetter<object> Setter { get; }
}
}

34
src/Avalonia.Base/PriorityValue.cs

@ -30,7 +30,9 @@ namespace Avalonia
private readonly SingleOrDictionary<int, PriorityLevel> _levels = new SingleOrDictionary<int, PriorityLevel>();
private readonly Func<object, object> _validate;
private readonly SetAndNotifyCallback<(object, int)> _setAndNotifyCallback;
private (object value, int priority) _value;
private DeferredSetter<object> _setter;
/// <summary>
/// Initializes a new instance of the <see cref="PriorityValue"/> class.
@ -50,6 +52,7 @@ namespace Avalonia
_valueType = valueType;
_value = (AvaloniaProperty.UnsetValue, int.MaxValue);
_validate = validate;
_setAndNotifyCallback = SetAndNotify;
}
/// <summary>
@ -242,22 +245,22 @@ namespace Avalonia
/// <param name="priority">The priority level that the value came from.</param>
private void UpdateValue(object value, int priority)
{
Owner.Setter.SetAndNotify(Property,
ref _value,
UpdateCore,
(value, priority));
}
var newValue = (value, priority);
if (newValue == _value)
{
return;
}
private bool UpdateCore(
object update,
ref (object value, int priority) backing,
Action<Action> notify)
=> UpdateCore(((object, int))update, ref backing, notify);
if (_setter == null)
{
_setter = Owner.GetNonDirectDeferredSetter(Property);
}
_setter.SetAndNotifyCallback(Property, _setAndNotifyCallback, ref _value, newValue);
}
private bool UpdateCore(
(object value, int priority) update,
ref (object value, int priority) backing,
Action<Action> notify)
private void SetAndNotify(AvaloniaProperty property, ref (object value, int priority) backing, (object value, int priority) update)
{
var val = update.value;
var notification = val as BindingNotification;
@ -286,7 +289,7 @@ namespace Avalonia
if (notification == null || notification.HasValue)
{
notify(() => Owner?.Changed(Property, ValuePriority, old, Value));
Owner?.Changed(Property, ValuePriority, old, Value);
}
if (notification != null)
@ -305,7 +308,6 @@ namespace Avalonia
val,
val?.GetType());
}
return true;
}
}
}

150
src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs

@ -0,0 +1,150 @@
// 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.Generic;
namespace Avalonia.Utilities
{
/// <summary>
/// Stores values with <see cref="AvaloniaProperty"/> as key.
/// </summary>
/// <typeparam name="TValue">Stored value type.</typeparam>
internal sealed class AvaloniaPropertyValueStore<TValue>
{
private Entry[] _entries;
public AvaloniaPropertyValueStore()
{
// The last item in the list is always int.MaxValue
_entries = new[] { new Entry { PropertyId = int.MaxValue, Value = default } };
}
private (int, bool) TryFindEntry(int propertyId)
{
if (_entries.Length <= 12)
{
// For small lists, we use an optimized linear search. Since the last item in the list
// is always int.MaxValue, we can skip a conditional branch in each iteration.
// By unrolling the loop, we can skip another unconditional branch in each iteration.
if (_entries[0].PropertyId >= propertyId)
return (0, _entries[0].PropertyId == propertyId);
if (_entries[1].PropertyId >= propertyId)
return (1, _entries[1].PropertyId == propertyId);
if (_entries[2].PropertyId >= propertyId)
return (2, _entries[2].PropertyId == propertyId);
if (_entries[3].PropertyId >= propertyId)
return (3, _entries[3].PropertyId == propertyId);
if (_entries[4].PropertyId >= propertyId)
return (4, _entries[4].PropertyId == propertyId);
if (_entries[5].PropertyId >= propertyId)
return (5, _entries[5].PropertyId == propertyId);
if (_entries[6].PropertyId >= propertyId)
return (6, _entries[6].PropertyId == propertyId);
if (_entries[7].PropertyId >= propertyId)
return (7, _entries[7].PropertyId == propertyId);
if (_entries[8].PropertyId >= propertyId)
return (8, _entries[8].PropertyId == propertyId);
if (_entries[9].PropertyId >= propertyId)
return (9, _entries[9].PropertyId == propertyId);
if (_entries[10].PropertyId >= propertyId)
return (10, _entries[10].PropertyId == propertyId);
}
else
{
int low = 0;
int high = _entries.Length;
int id;
while (high - low > 3)
{
int pivot = (high + low) / 2;
id = _entries[pivot].PropertyId;
if (propertyId == id)
return (pivot, true);
if (propertyId <= id)
high = pivot;
else
low = pivot + 1;
}
do
{
id = _entries[low].PropertyId;
if (id == propertyId)
return (low, true);
if (id > propertyId)
break;
++low;
}
while (low < high);
}
return (0, false);
}
public bool TryGetValue(AvaloniaProperty property, out TValue value)
{
(int index, bool found) = TryFindEntry(property.Id);
if (!found)
{
value = default;
return false;
}
value = _entries[index].Value;
return true;
}
public void AddValue(AvaloniaProperty property, TValue value)
{
Entry[] entries = new Entry[_entries.Length + 1];
for (int i = 0; i < _entries.Length; ++i)
{
if (_entries[i].PropertyId > property.Id)
{
if (i > 0)
{
Array.Copy(_entries, 0, entries, 0, i);
}
entries[i] = new Entry { PropertyId = property.Id, Value = value };
Array.Copy(_entries, i, entries, i + 1, _entries.Length - i);
break;
}
}
_entries = entries;
}
public void SetValue(AvaloniaProperty property, TValue value)
{
_entries[TryFindEntry(property.Id).Item1].Value = value;
}
public Dictionary<AvaloniaProperty, TValue> ToDictionary()
{
var dict = new Dictionary<AvaloniaProperty, TValue>(_entries.Length - 1);
for (int i = 0; i < _entries.Length - 1; ++i)
{
dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value);
}
return dict;
}
private struct Entry
{
internal int PropertyId;
internal TValue Value;
}
}
}

206
src/Avalonia.Base/Utilities/DeferredSetter.cs

@ -1,168 +1,122 @@
using System;
using System.Collections.Generic;
// 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.Utilities
{
/// <summary>
/// Callback invoked when deferred setter wants to set a value.
/// </summary>
/// <typeparam name="TValue">Value type.</typeparam>
/// <param name="property">Property being set.</param>
/// <param name="backing">Backing field reference.</param>
/// <param name="value">New value.</param>
internal delegate void SetAndNotifyCallback<TValue>(AvaloniaProperty property, ref TValue backing, TValue value);
/// <summary>
/// A utility class to enable deferring assignment until after property-changed notifications are sent.
/// Used to fix #855.
/// </summary>
/// <typeparam name="TSetRecord">The type of value with which to track the delayed assignment.</typeparam>
class DeferredSetter<TSetRecord>
internal sealed class DeferredSetter<TSetRecord>
{
private struct NotifyDisposable : IDisposable
private readonly SingleOrQueue<TSetRecord> _pendingValues;
private bool _isNotifying;
public DeferredSetter()
{
private readonly SettingStatus status;
_pendingValues = new SingleOrQueue<TSetRecord>();
}
internal NotifyDisposable(SettingStatus status)
{
this.status = status;
status.Notifying = true;
}
private static void SetAndRaisePropertyChanged(AvaloniaObject source, AvaloniaProperty<TSetRecord> property, ref TSetRecord backing, TSetRecord value)
{
var old = backing;
public void Dispose()
{
status.Notifying = false;
}
backing = value;
source.RaisePropertyChanged(property, old, value);
}
/// <summary>
/// Information on current setting/notification status of a property.
/// </summary>
private class SettingStatus
public bool SetAndNotify(
AvaloniaObject source,
AvaloniaProperty<TSetRecord> property,
ref TSetRecord backing,
TSetRecord value)
{
public bool Notifying { get; set; }
private SingleOrQueue<TSetRecord> pendingValues;
public SingleOrQueue<TSetRecord> PendingValues
if (!_isNotifying)
{
get
using (new NotifyDisposable(this))
{
return pendingValues ?? (pendingValues = new SingleOrQueue<TSetRecord>());
SetAndRaisePropertyChanged(source, property, ref backing, value);
}
}
}
private Dictionary<AvaloniaProperty, SettingStatus> _setRecords;
private Dictionary<AvaloniaProperty, SettingStatus> SetRecords
=> _setRecords ?? (_setRecords = new Dictionary<AvaloniaProperty, SettingStatus>());
if (!_pendingValues.Empty)
{
using (new NotifyDisposable(this))
{
while (!_pendingValues.Empty)
{
SetAndRaisePropertyChanged(source, property, ref backing, _pendingValues.Dequeue());
}
}
}
private SettingStatus GetOrCreateStatus(AvaloniaProperty property)
{
if (!SetRecords.TryGetValue(property, out var status))
{
status = new SettingStatus();
SetRecords.Add(property, status);
return true;
}
return status;
_pendingValues.Enqueue(value);
return false;
}
/// <summary>
/// Mark the property as currently notifying.
/// </summary>
/// <param name="property">The property to mark as notifying.</param>
/// <returns>Returns a disposable that when disposed, marks the property as done notifying.</returns>
private NotifyDisposable MarkNotifying(AvaloniaProperty property)
public bool SetAndNotifyCallback<TValue>(AvaloniaProperty property, SetAndNotifyCallback<TValue> setAndNotifyCallback, ref TValue backing, TValue value)
where TValue : TSetRecord
{
Contract.Requires<InvalidOperationException>(!IsNotifying(property));
SettingStatus status = GetOrCreateStatus(property);
return new NotifyDisposable(status);
}
if (!_isNotifying)
{
using (new NotifyDisposable(this))
{
setAndNotifyCallback(property, ref backing, value);
}
/// <summary>
/// Check if the property is currently notifying listeners.
/// </summary>
/// <param name="property">The property.</param>
/// <returns>If the property is currently notifying listeners.</returns>
private bool IsNotifying(AvaloniaProperty property)
=> SetRecords.TryGetValue(property, out var value) && value.Notifying;
if (!_pendingValues.Empty)
{
using (new NotifyDisposable(this))
{
while (!_pendingValues.Empty)
{
setAndNotifyCallback(property, ref backing, (TValue) _pendingValues.Dequeue());
}
}
}
/// <summary>
/// Add a pending assignment for the property.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="value">The value to assign.</param>
private void AddPendingSet(AvaloniaProperty property, TSetRecord value)
{
Contract.Requires<InvalidOperationException>(IsNotifying(property));
return true;
}
GetOrCreateStatus(property).PendingValues.Enqueue(value);
}
_pendingValues.Enqueue(value);
/// <summary>
/// Checks if there are any pending assignments for the property.
/// </summary>
/// <param name="property">The property to check.</param>
/// <returns>If the property has any pending assignments.</returns>
private bool HasPendingSet(AvaloniaProperty property)
{
return SetRecords.TryGetValue(property, out var status) && !status.PendingValues.Empty;
return false;
}
/// <summary>
/// Gets the first pending assignment for the property.
/// Disposable that marks the property as currently notifying.
/// When disposed, marks the property as done notifying.
/// </summary>
/// <param name="property">The property to check.</param>
/// <returns>The first pending assignment for the property.</returns>
private TSetRecord GetFirstPendingSet(AvaloniaProperty property)
private readonly struct NotifyDisposable : IDisposable
{
return GetOrCreateStatus(property).PendingValues.Dequeue();
}
public delegate bool SetterDelegate<TValue>(TSetRecord record, ref TValue backing, Action<Action> notifyCallback);
private readonly DeferredSetter<TSetRecord> _setter;
/// <summary>
/// Set the property and notify listeners while ensuring we don't get into a stack overflow as happens with #855 and #824
/// </summary>
/// <param name="property">The property to set.</param>
/// <param name="backing">The backing field for the property</param>
/// <param name="setterCallback">
/// A callback that actually sets the property.
/// The first parameter is the value to set, and the second is a wrapper that takes a callback that sends the property-changed notification.
/// </param>
/// <param name="value">The value to try to set.</param>
public bool SetAndNotify<TValue>(
AvaloniaProperty property,
ref TValue backing,
SetterDelegate<TValue> setterCallback,
TSetRecord value)
{
Contract.Requires<ArgumentNullException>(setterCallback != null);
if (!IsNotifying(property))
internal NotifyDisposable(DeferredSetter<TSetRecord> setter)
{
bool updated = false;
if (!object.Equals(value, backing))
{
updated = setterCallback(value, ref backing, notification =>
{
using (MarkNotifying(property))
{
notification();
}
});
}
while (HasPendingSet(property))
{
updated |= setterCallback(GetFirstPendingSet(property), ref backing, notification =>
{
using (MarkNotifying(property))
{
notification();
}
});
}
return updated;
_setter = setter;
_setter._isNotifying = true;
}
else if(!object.Equals(value, backing))
public void Dispose()
{
AddPendingSet(property, value);
_setter._isNotifying = false;
}
return false;
}
}
}

152
src/Avalonia.Base/ValueStore.cs

@ -7,21 +7,15 @@ namespace Avalonia
{
internal class ValueStore : IPriorityValueOwner
{
private struct Entry
{
internal int PropertyId;
internal object Value;
}
private readonly AvaloniaPropertyValueStore<object> _propertyValues;
private readonly AvaloniaPropertyValueStore<object> _deferredSetters;
private readonly AvaloniaObject _owner;
private Entry[] _entries;
public ValueStore(AvaloniaObject owner)
{
_owner = owner;
// The last item in the list is always int.MaxValue
_entries = new[] { new Entry { PropertyId = int.MaxValue, Value = null } };
_propertyValues = new AvaloniaPropertyValueStore<object>();
_deferredSetters = new AvaloniaPropertyValueStore<object>();
}
public IDisposable AddBinding(
@ -31,7 +25,7 @@ namespace Avalonia
{
PriorityValue priorityValue;
if (TryGetValue(property, out var v))
if (_propertyValues.TryGetValue(property, out var v))
{
priorityValue = v as PriorityValue;
@ -39,13 +33,13 @@ namespace Avalonia
{
priorityValue = CreatePriorityValue(property);
priorityValue.SetValue(v, (int)BindingPriority.LocalValue);
SetValueInternal(property, priorityValue);
_propertyValues.SetValue(property, priorityValue);
}
}
else
{
priorityValue = CreatePriorityValue(property);
AddValueInternal(property, priorityValue);
_propertyValues.AddValue(property, priorityValue);
}
return priorityValue.Add(source, (int)priority);
@ -55,7 +49,7 @@ namespace Avalonia
{
PriorityValue priorityValue;
if (TryGetValue(property, out var v))
if (_propertyValues.TryGetValue(property, out var v))
{
priorityValue = v as PriorityValue;
@ -63,7 +57,7 @@ namespace Avalonia
{
if (priority == (int)BindingPriority.LocalValue)
{
SetValueInternal(property, Validate(property, value));
_propertyValues.SetValue(property, Validate(property, value));
Changed(property, priority, v, value);
return;
}
@ -71,7 +65,7 @@ namespace Avalonia
{
priorityValue = CreatePriorityValue(property);
priorityValue.SetValue(v, (int)BindingPriority.LocalValue);
SetValueInternal(property, priorityValue);
_propertyValues.SetValue(property, priorityValue);
}
}
}
@ -84,14 +78,14 @@ namespace Avalonia
if (priority == (int)BindingPriority.LocalValue)
{
AddValueInternal(property, Validate(property, value));
_propertyValues.AddValue(property, Validate(property, value));
Changed(property, priority, AvaloniaProperty.UnsetValue, value);
return;
}
else
{
priorityValue = CreatePriorityValue(property);
AddValueInternal(property, priorityValue);
_propertyValues.AddValue(property, priorityValue);
}
}
@ -110,14 +104,9 @@ namespace Avalonia
public IDictionary<AvaloniaProperty, object> GetSetValues()
{
var dict = new Dictionary<AvaloniaProperty, object>(_entries.Length - 1);
for (int i = 0; i < _entries.Length - 1; ++i)
{
dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value);
}
return dict;
return _propertyValues.ToDictionary();
}
public void LogError(AvaloniaProperty property, Exception e)
{
_owner.LogBindingError(property, e);
@ -127,7 +116,7 @@ namespace Avalonia
{
var result = AvaloniaProperty.UnsetValue;
if (TryGetValue(property, out var value))
if (_propertyValues.TryGetValue(property, out var value))
{
result = (value is PriorityValue priorityValue) ? priorityValue.Value : value;
}
@ -137,12 +126,12 @@ namespace Avalonia
public bool IsAnimating(AvaloniaProperty property)
{
return TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating;
return _propertyValues.TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating;
}
public bool IsSet(AvaloniaProperty property)
{
if (TryGetValue(property, out var value))
if (_propertyValues.TryGetValue(property, out var value))
{
return ((value as PriorityValue)?.Value ?? value) != AvaloniaProperty.UnsetValue;
}
@ -152,7 +141,7 @@ namespace Avalonia
public void Revalidate(AvaloniaProperty property)
{
if (TryGetValue(property, out var value))
if (_propertyValues.TryGetValue(property, out var value))
{
(value as PriorityValue)?.Revalidate();
}
@ -189,113 +178,28 @@ namespace Avalonia
return value;
}
private DeferredSetter<object> _deferredSetter;
public DeferredSetter<object> Setter
private DeferredSetter<T> GetDeferredSetter<T>(AvaloniaProperty property)
{
get
if (_deferredSetters.TryGetValue(property, out var deferredSetter))
{
return _deferredSetter ??
(_deferredSetter = new DeferredSetter<object>());
return (DeferredSetter<T>)deferredSetter;
}
}
private bool TryGetValue(AvaloniaProperty property, out object value)
{
(int index, bool found) = TryFindEntry(property.Id);
if (!found)
{
value = null;
return false;
}
value = _entries[index].Value;
return true;
}
private void AddValueInternal(AvaloniaProperty property, object value)
{
Entry[] entries = new Entry[_entries.Length + 1];
for (int i = 0; i < _entries.Length; ++i)
{
if (_entries[i].PropertyId > property.Id)
{
if (i > 0)
{
Array.Copy(_entries, 0, entries, 0, i);
}
var newDeferredSetter = new DeferredSetter<T>();
entries[i] = new Entry { PropertyId = property.Id, Value = value };
Array.Copy(_entries, i, entries, i + 1, _entries.Length - i);
break;
}
}
_deferredSetters.AddValue(property, newDeferredSetter);
_entries = entries;
return newDeferredSetter;
}
private void SetValueInternal(AvaloniaProperty property, object value)
public DeferredSetter<object> GetNonDirectDeferredSetter(AvaloniaProperty property)
{
_entries[TryFindEntry(property.Id).Item1].Value = value;
return GetDeferredSetter<object>(property);
}
private (int, bool) TryFindEntry(int propertyId)
public DeferredSetter<T> GetDirectDeferredSetter<T>(AvaloniaProperty<T> property)
{
if (_entries.Length <= 12)
{
// For small lists, we use an optimized linear search. Since the last item in the list
// is always int.MaxValue, we can skip a conditional branch in each iteration.
// By unrolling the loop, we can skip another unconditional branch in each iteration.
if (_entries[0].PropertyId >= propertyId) return (0, _entries[0].PropertyId == propertyId);
if (_entries[1].PropertyId >= propertyId) return (1, _entries[1].PropertyId == propertyId);
if (_entries[2].PropertyId >= propertyId) return (2, _entries[2].PropertyId == propertyId);
if (_entries[3].PropertyId >= propertyId) return (3, _entries[3].PropertyId == propertyId);
if (_entries[4].PropertyId >= propertyId) return (4, _entries[4].PropertyId == propertyId);
if (_entries[5].PropertyId >= propertyId) return (5, _entries[5].PropertyId == propertyId);
if (_entries[6].PropertyId >= propertyId) return (6, _entries[6].PropertyId == propertyId);
if (_entries[7].PropertyId >= propertyId) return (7, _entries[7].PropertyId == propertyId);
if (_entries[8].PropertyId >= propertyId) return (8, _entries[8].PropertyId == propertyId);
if (_entries[9].PropertyId >= propertyId) return (9, _entries[9].PropertyId == propertyId);
if (_entries[10].PropertyId >= propertyId) return (10, _entries[10].PropertyId == propertyId);
}
else
{
int low = 0;
int high = _entries.Length;
int id;
while (high - low > 3)
{
int pivot = (high + low) / 2;
id = _entries[pivot].PropertyId;
if (propertyId == id)
return (pivot, true);
if (propertyId <= id)
high = pivot;
else
low = pivot + 1;
}
do
{
id = _entries[low].PropertyId;
if (id == propertyId)
return (low, true);
if (id > propertyId)
break;
++low;
}
while (low < high);
}
return (0, false);
return GetDeferredSetter<T>(property);
}
}
}

3
src/Avalonia.Controls.DataGrid/Themes/Default.xaml

@ -195,7 +195,6 @@
<Setter Property="GridLinesVisibility" Value="Vertical" />
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource ThemeBorderLightBrush}" />
<Setter Property="VerticalGridLinesBrush" Value="{DynamicResource ThemeBorderLightBrush}" />
<Setter Property="HeadersVisibility" Value="Column" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderDarkBrush}"/>
<Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}" />
<Setter Property="DropLocationIndicatorTemplate">
@ -230,4 +229,4 @@
</ControlTemplate>
</Setter>
</Style>
</Styles>
</Styles>

26
src/Avalonia.Controls/AutoCompleteBox.cs

@ -345,7 +345,6 @@ namespace Avalonia.Controls
/// </summary>
private IDisposable _collectionChangeSubscription;
private IMemberSelector _valueMemberSelector;
private Func<string, CancellationToken, Task<IEnumerable<object>>> _asyncPopulator;
private CancellationTokenSource _populationCancellationTokenSource;
@ -541,12 +540,6 @@ namespace Avalonia.Controls
o => o.Items,
(o, v) => o.Items = v);
public static readonly DirectProperty<AutoCompleteBox, IMemberSelector> ValueMemberSelectorProperty =
AvaloniaProperty.RegisterDirect<AutoCompleteBox, IMemberSelector>(
nameof(ValueMemberSelector),
o => o.ValueMemberSelector,
(o, v) => o.ValueMemberSelector = v);
public static readonly DirectProperty<AutoCompleteBox, Func<string, CancellationToken, Task<IEnumerable<object>>>> AsyncPopulatorProperty =
AvaloniaProperty.RegisterDirect<AutoCompleteBox, Func<string, CancellationToken, Task<IEnumerable<object>>>>(
nameof(AsyncPopulator),
@ -958,20 +951,6 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Gets or sets the MemberSelector that is used to get values for
/// display in the text portion of the
/// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.
/// </summary>
/// <value>The MemberSelector that is used to get values for display in
/// the text portion of the
/// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control.</value>
public IMemberSelector ValueMemberSelector
{
get { return _valueMemberSelector; }
set { SetAndRaise(ValueMemberSelectorProperty, ref _valueMemberSelector, value); }
}
/// <summary>
/// Gets or sets the selected item in the drop-down.
/// </summary>
@ -1841,11 +1820,6 @@ namespace Avalonia.Controls
return _valueBindingEvaluator.GetDynamicValue(value) ?? String.Empty;
}
if (_valueMemberSelector != null)
{
value = _valueMemberSelector.Select(value);
}
return value == null ? String.Empty : value.ToString();
}

3
src/Avalonia.Controls/ComboBox.cs

@ -333,8 +333,7 @@ namespace Avalonia.Controls
}
else
{
var selector = MemberSelector;
SelectionBoxItem = selector != null ? selector.Select(item) : item;
SelectionBoxItem = item;
}
}

39
src/Avalonia.Controls/ContextMenu.cs

@ -1,12 +1,12 @@
using System;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
namespace Avalonia.Controls
@ -90,9 +90,14 @@ namespace Avalonia.Controls
/// <param name="control">The control.</param>
public void Open(Control control)
{
if (IsOpen)
{
return;
}
if (_popup == null)
{
_popup = new Popup()
_popup = new Popup
{
PlacementMode = PlacementMode.Pointer,
PlacementTarget = control,
@ -107,7 +112,14 @@ namespace Avalonia.Controls
((ISetLogicalParent)_popup).SetParent(control);
_popup.Child = this;
_popup.IsOpen = true;
IsOpen = true;
RaiseEvent(new RoutedEventArgs
{
RoutedEvent = MenuOpenedEvent,
Source = this,
});
}
/// <summary>
@ -115,13 +127,15 @@ namespace Avalonia.Controls
/// </summary>
public override void Close()
{
if (!IsOpen)
{
return;
}
if (_popup != null && _popup.IsVisible)
{
_popup.IsOpen = false;
}
SelectedIndex = -1;
IsOpen = false;
}
protected override IItemContainerGenerator CreateItemContainerGenerator()
@ -129,6 +143,18 @@ namespace Avalonia.Controls
return new MenuItemContainerGenerator(this);
}
private void CloseCore()
{
SelectedIndex = -1;
IsOpen = false;
RaiseEvent(new RoutedEventArgs
{
RoutedEvent = MenuClosedEvent,
Source = this,
});
}
private void PopupOpened(object sender, EventArgs e)
{
Focus();
@ -145,8 +171,7 @@ namespace Avalonia.Controls
i.IsSubMenuOpen = false;
}
contextMenu.IsOpen = false;
contextMenu.SelectedIndex = -1;
contextMenu.CloseCore();
}
}

12
src/Avalonia.Controls/Generators/IItemContainerGenerator.cs

@ -49,12 +49,8 @@ namespace Avalonia.Controls.Generators
/// The index of the item of data in the control's items.
/// </param>
/// <param name="item">The item.</param>
/// <param name="selector">An optional member selector.</param>
/// <returns>The created controls.</returns>
ItemContainerInfo Materialize(
int index,
object item,
IMemberSelector selector);
ItemContainerInfo Materialize(int index, object item);
/// <summary>
/// Removes a set of created containers.
@ -84,11 +80,7 @@ namespace Avalonia.Controls.Generators
/// <returns>The removed containers.</returns>
IEnumerable<ItemContainerInfo> RemoveRange(int startingIndex, int count);
bool TryRecycle(
int oldIndex,
int newIndex,
object item,
IMemberSelector selector);
bool TryRecycle(int oldIndex, int newIndex, object item);
/// <summary>
/// Clears all created containers and returns the removed controls.

17
src/Avalonia.Controls/Generators/ItemContainerGenerator.cs

@ -54,13 +54,9 @@ namespace Avalonia.Controls.Generators
public virtual Type ContainerType => null;
/// <inheritdoc/>
public ItemContainerInfo Materialize(
int index,
object item,
IMemberSelector selector)
public ItemContainerInfo Materialize(int index, object item)
{
var i = selector != null ? selector.Select(item) : item;
var container = new ItemContainerInfo(CreateContainer(i), item, index);
var container = new ItemContainerInfo(CreateContainer(item), item, index);
_containers.Add(container.Index, container);
Materialized?.Invoke(this, new ItemContainerEventArgs(container));
@ -138,14 +134,7 @@ namespace Avalonia.Controls.Generators
}
/// <inheritdoc/>
public virtual bool TryRecycle(
int oldIndex,
int newIndex,
object item,
IMemberSelector selector)
{
return false;
}
public virtual bool TryRecycle(int oldIndex, int newIndex, object item) => false;
/// <inheritdoc/>
public virtual IEnumerable<ItemContainerInfo> Clear()

14
src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs

@ -79,11 +79,7 @@ namespace Avalonia.Controls.Generators
}
/// <inheritdoc/>
public override bool TryRecycle(
int oldIndex,
int newIndex,
object item,
IMemberSelector selector)
public override bool TryRecycle(int oldIndex, int newIndex, object item)
{
var container = ContainerFromIndex(oldIndex);
@ -92,16 +88,14 @@ namespace Avalonia.Controls.Generators
throw new IndexOutOfRangeException("Could not recycle container: not materialized.");
}
var i = selector != null ? selector.Select(item) : item;
container.SetValue(ContentProperty, i);
container.SetValue(ContentProperty, item);
if (!(item is IControl))
{
container.DataContext = i;
container.DataContext = item;
}
var info = MoveContainer(oldIndex, newIndex, i);
var info = MoveContainer(oldIndex, newIndex, item);
RaiseRecycled(new ItemContainerEventArgs(info));
return true;

5
src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs

@ -117,10 +117,7 @@ namespace Avalonia.Controls.Generators
return base.RemoveRange(startingIndex, count);
}
public override bool TryRecycle(int oldIndex, int newIndex, object item, IMemberSelector selector)
{
return false;
}
public override bool TryRecycle(int oldIndex, int newIndex, object item) => false;
class WrapperTreeDataTemplate : ITreeDataTemplate
{

15
src/Avalonia.Controls/ItemsControl.cs

@ -54,12 +54,6 @@ namespace Avalonia.Controls
public static readonly StyledProperty<IDataTemplate> ItemTemplateProperty =
AvaloniaProperty.Register<ItemsControl, IDataTemplate>(nameof(ItemTemplate));
/// <summary>
/// Defines the <see cref="MemberSelector"/> property.
/// </summary>
public static readonly StyledProperty<IMemberSelector> MemberSelectorProperty =
AvaloniaProperty.Register<ItemsControl, IMemberSelector>(nameof(MemberSelector));
private IEnumerable _items = new AvaloniaList<object>();
private int _itemCount;
private IItemContainerGenerator _itemContainerGenerator;
@ -144,15 +138,6 @@ namespace Avalonia.Controls
set { SetValue(ItemTemplateProperty, value); }
}
/// <summary>
/// Selects a member from <see cref="Items"/> to use as the list item.
/// </summary>
public IMemberSelector MemberSelector
{
get { return GetValue(MemberSelectorProperty); }
set { SetValue(MemberSelectorProperty, value); }
}
/// <summary>
/// Gets the items presenter control.
/// </summary>

2
src/Avalonia.Controls/LayoutTransformControl.cs

@ -45,7 +45,7 @@ namespace Avalonia.Controls
}
/// <summary>
/// Utilize the <see cref="RenderTransformProperty"/> for layout transforms.
/// Utilize the <see cref="Visual.RenderTransformProperty"/> for layout transforms.
/// </summary>
public bool UseRenderTransform
{

8
src/Avalonia.Controls/ListBox.cs

@ -68,7 +68,13 @@ namespace Avalonia.Controls
/// <inheritdoc/>
public new IList SelectedItems => base.SelectedItems;
/// <inheritdoc/>
/// <summary>
/// Gets or sets the selection mode.
/// </summary>
/// <remarks>
/// Note that the selection mode only applies to selections made via user interaction.
/// Multiple selections can be made programatically regardless of the value of this property.
/// </remarks>
public new SelectionMode SelectionMode
{
get { return base.SelectionMode; }

46
src/Avalonia.Controls/Menu.cs

@ -40,37 +40,41 @@ namespace Avalonia.Controls
/// <inheritdoc/>
public override void Close()
{
if (IsOpen)
if (!IsOpen)
{
foreach (var i in ((IMenu)this).SubItems)
{
i.Close();
}
IsOpen = false;
SelectedIndex = -1;
return;
}
RaiseEvent(new RoutedEventArgs
{
RoutedEvent = MenuClosedEvent,
Source = this,
});
foreach (var i in ((IMenu)this).SubItems)
{
i.Close();
}
IsOpen = false;
SelectedIndex = -1;
RaiseEvent(new RoutedEventArgs
{
RoutedEvent = MenuClosedEvent,
Source = this,
});
}
/// <inheritdoc/>
public override void Open()
{
if (!IsOpen)
if (IsOpen)
{
IsOpen = true;
RaiseEvent(new RoutedEventArgs
{
RoutedEvent = MenuOpenedEvent,
Source = this,
});
return;
}
IsOpen = true;
RaiseEvent(new RoutedEventArgs
{
RoutedEvent = MenuOpenedEvent,
Source = this,
});
}
/// <inheritdoc/>

5
src/Avalonia.Controls/MenuBase.cs

@ -7,7 +7,6 @@ using System.Linq;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
@ -31,13 +30,13 @@ namespace Avalonia.Controls
/// Defines the <see cref="MenuOpened"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> MenuOpenedEvent =
RoutedEvent.Register<MenuItem, RoutedEventArgs>(nameof(MenuOpened), RoutingStrategies.Bubble);
RoutedEvent.Register<MenuBase, RoutedEventArgs>(nameof(MenuOpened), RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="MenuClosed"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> MenuClosedEvent =
RoutedEvent.Register<MenuItem, RoutedEventArgs>(nameof(MenuClosed), RoutingStrategies.Bubble);
RoutedEvent.Register<MenuBase, RoutedEventArgs>(nameof(MenuClosed), RoutingStrategies.Bubble);
private bool _isOpen;

85
src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs

@ -9,94 +9,69 @@ using Avalonia.Threading;
namespace Avalonia.Controls.Platform
{
public class InternalPlatformThreadingInterface : IPlatformThreadingInterface, IRenderTimer
public class InternalPlatformThreadingInterface : IPlatformThreadingInterface
{
public InternalPlatformThreadingInterface()
{
TlsCurrentThreadIsLoopThread = true;
StartTimer(
DispatcherPriority.Render,
new TimeSpan(0, 0, 0, 0, 66),
() => Tick?.Invoke(TimeSpan.FromMilliseconds(Environment.TickCount)));
}
private readonly AutoResetEvent _signaled = new AutoResetEvent(false);
private readonly AutoResetEvent _queued = new AutoResetEvent(false);
private readonly Queue<Action> _actions = new Queue<Action>();
public void RunLoop(CancellationToken cancellationToken)
{
var handles = new[] {_signaled, _queued};
while (true)
{
if (0 == WaitHandle.WaitAny(handles))
Signaled?.Invoke(null);
else
{
while (true)
{
Action item;
lock (_actions)
if (_actions.Count == 0)
break;
else
item = _actions.Dequeue();
item();
}
}
Signaled?.Invoke(null);
_signaled.WaitOne();
}
}
public void Send(Action cb)
{
lock (_actions)
{
_actions.Enqueue(cb);
_queued.Set();
}
}
class WatTimer : IDisposable
class TimerImpl : IDisposable
{
private readonly IDisposable _timer;
private readonly DispatcherPriority _priority;
private readonly TimeSpan _interval;
private readonly Action _tick;
private Timer _timer;
private GCHandle _handle;
public WatTimer(IDisposable timer)
public TimerImpl(DispatcherPriority priority, TimeSpan interval, Action tick)
{
_timer = timer;
_priority = priority;
_interval = interval;
_tick = tick;
_timer = new Timer(OnTimer, null, interval, TimeSpan.FromMilliseconds(-1));
_handle = GCHandle.Alloc(_timer);
}
private void OnTimer(object state)
{
if (_timer == null)
return;
Dispatcher.UIThread.Post(() =>
{
if (_timer == null)
return;
_tick();
_timer?.Change(_interval, TimeSpan.FromMilliseconds(-1));
});
}
public void Dispose()
{
_handle.Free();
_timer.Dispose();
_timer = null;
}
}
public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)
{
return new WatTimer(new System.Threading.Timer(delegate
{
var tcs = new TaskCompletionSource<int>();
Send(() =>
{
try
{
tick();
}
finally
{
tcs.SetResult(0);
}
});
tcs.Task.Wait();
}, null, TimeSpan.Zero, interval));
return new TimerImpl(priority, interval, tick);
}
public void Signal(DispatcherPriority prio)

2
src/Avalonia.Controls/Presenters/CarouselPresenter.cs

@ -213,7 +213,7 @@ namespace Avalonia.Controls.Presenters
if (container == null && IsVirtualized)
{
var item = Items.Cast<object>().ElementAt(index);
var materialized = ItemContainerGenerator.Materialize(index, item, MemberSelector);
var materialized = ItemContainerGenerator.Materialize(index, item);
Panel.Children.Add(materialized.ContainerControl);
container = materialized.ContainerControl;
}

2
src/Avalonia.Controls/Presenters/ItemContainerSync.cs

@ -88,7 +88,7 @@ namespace Avalonia.Controls.Presenters
foreach (var item in items)
{
var i = generator.Materialize(index++, item, owner.MemberSelector);
var i = generator.Materialize(index++, item);
if (i.ContainerControl != null)
{

2
src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs

@ -90,7 +90,7 @@ namespace Avalonia.Controls.Presenters
foreach (var item in items)
{
var i = generator.Materialize(index++, item, Owner.MemberSelector);
var i = generator.Materialize(index++, item);
if (i.ContainerControl != null)
{

9
src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs

@ -314,7 +314,6 @@ namespace Avalonia.Controls.Presenters
if (!panel.IsFull && Items != null && panel.IsAttachedToVisualTree)
{
var memberSelector = Owner.MemberSelector;
var index = NextIndex;
var step = 1;
@ -337,7 +336,7 @@ namespace Avalonia.Controls.Presenters
}
}
var materialized = generator.Materialize(index, Items.ElementAt(index), memberSelector);
var materialized = generator.Materialize(index, Items.ElementAt(index));
if (step == 1)
{
@ -383,7 +382,6 @@ namespace Avalonia.Controls.Presenters
{
var panel = VirtualizingPanel;
var generator = Owner.ItemContainerGenerator;
var selector = Owner.MemberSelector;
var containers = generator.Containers.ToList();
var itemIndex = FirstIndex;
@ -393,7 +391,7 @@ namespace Avalonia.Controls.Presenters
if (!object.Equals(container.Item, item))
{
if (!generator.TryRecycle(itemIndex, itemIndex, item, selector))
if (!generator.TryRecycle(itemIndex, itemIndex, item))
{
throw new NotImplementedException();
}
@ -420,7 +418,6 @@ namespace Avalonia.Controls.Presenters
{
var panel = VirtualizingPanel;
var generator = Owner.ItemContainerGenerator;
var selector = Owner.MemberSelector;
//validate delta it should never overflow last index or generate index < 0
delta = MathUtilities.Clamp(delta, -FirstIndex, ItemCount - FirstIndex - panel.Children.Count);
@ -437,7 +434,7 @@ namespace Avalonia.Controls.Presenters
var item = Items.ElementAt(newItemIndex);
if (!generator.TryRecycle(oldItemIndex, newItemIndex, item, selector))
if (!generator.TryRecycle(oldItemIndex, newItemIndex, item))
{
throw new NotImplementedException();
}

15
src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs

@ -35,12 +35,6 @@ namespace Avalonia.Controls.Presenters
public static readonly StyledProperty<IDataTemplate> ItemTemplateProperty =
ItemsControl.ItemTemplateProperty.AddOwner<ItemsPresenterBase>();
/// <summary>
/// Defines the <see cref="MemberSelector"/> property.
/// </summary>
public static readonly StyledProperty<IMemberSelector> MemberSelectorProperty =
ItemsControl.MemberSelectorProperty.AddOwner<ItemsPresenterBase>();
private IEnumerable _items;
private IDisposable _itemsSubscription;
private bool _createdPanel;
@ -127,15 +121,6 @@ namespace Avalonia.Controls.Presenters
set { SetValue(ItemTemplateProperty, value); }
}
/// <summary>
/// Selects a member from <see cref="Items"/> to use as the list item.
/// </summary>
public IMemberSelector MemberSelector
{
get { return GetValue(MemberSelectorProperty); }
set { SetValue(MemberSelectorProperty, value); }
}
/// <summary>
/// Gets the panel used to display the items.
/// </summary>

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

@ -6,7 +6,6 @@ using System.Linq;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
using Avalonia.VisualTree;
@ -270,9 +269,10 @@ namespace Avalonia.Controls.Primitives
_popupRoot.SnapInsideScreenEdges();
}
_ignoreIsOpenChanged = true;
IsOpen = true;
_ignoreIsOpenChanged = false;
using (BeginIgnoringIsOpen())
{
IsOpen = true;
}
Opened?.Invoke(this, EventArgs.Empty);
}
@ -305,7 +305,11 @@ namespace Avalonia.Controls.Primitives
_popupRoot.Hide();
}
IsOpen = false;
using (BeginIgnoringIsOpen())
{
IsOpen = false;
}
Closed?.Invoke(this, EventArgs.Empty);
}
@ -467,5 +471,26 @@ namespace Avalonia.Controls.Primitives
Close();
}
}
private IgnoreIsOpenScope BeginIgnoringIsOpen()
{
return new IgnoreIsOpenScope(this);
}
private readonly struct IgnoreIsOpenScope : IDisposable
{
private readonly Popup _owner;
public IgnoreIsOpenScope(Popup owner)
{
_owner = owner;
_owner._ignoreIsOpenChanged = true;
}
public void Dispose()
{
_owner._ignoreIsOpenChanged = false;
}
}
}
}

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

@ -222,6 +222,10 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Gets or sets the selection mode.
/// </summary>
/// <remarks>
/// Note that the selection mode only applies to selections made via user interaction.
/// Multiple selections can be made programatically regardless of the value of this property.
/// </remarks>
protected SelectionMode SelectionMode
{
get { return GetValue(SelectionModeProperty); }
@ -338,24 +342,36 @@ namespace Avalonia.Controls.Primitives
{
base.OnContainersMaterialized(e);
var selectedIndex = SelectedIndex;
var selectedContainer = e.Containers
.FirstOrDefault(x => (x.ContainerControl as ISelectable)?.IsSelected == true);
var resetSelectedItems = false;
if (selectedContainer != null)
foreach (var container in e.Containers)
{
SelectedIndex = selectedContainer.Index;
}
else if (selectedIndex >= e.StartingIndex &&
selectedIndex < e.StartingIndex + e.Containers.Count)
{
var container = e.Containers[selectedIndex - e.StartingIndex];
if ((container.ContainerControl as ISelectable)?.IsSelected == true)
{
if (SelectedIndex == -1)
{
SelectedIndex = container.Index;
}
else
{
if (_selection.Add(container.Index))
{
resetSelectedItems = true;
}
}
if (container.ContainerControl != null)
MarkContainerSelected(container.ContainerControl, true);
}
else if (_selection.Contains(container.Index))
{
MarkContainerSelected(container.ContainerControl, true);
}
}
if (resetSelectedItems)
{
ResetSelectedItems();
}
}
/// <inheritdoc/>
@ -469,11 +485,6 @@ namespace Avalonia.Controls.Primitives
/// </summary>
protected void SelectAll()
{
if ((SelectionMode & (SelectionMode.Multiple | SelectionMode.Toggle)) == 0)
{
throw new NotSupportedException("Multiple selection is not enabled on this control.");
}
UpdateSelectedItems(() =>
{
_selection.Clear();
@ -523,7 +534,14 @@ namespace Avalonia.Controls.Primitives
var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0);
var range = multi && rangeModifier;
if (range)
if (rightButton)
{
if (!_selection.Contains(index))
{
UpdateSelectedItem(index);
}
}
else if (range)
{
UpdateSelectedItems(() =>
{
@ -582,7 +600,7 @@ namespace Avalonia.Controls.Primitives
}
else
{
UpdateSelectedItem(index, !(rightButton && _selection.Contains(index)));
UpdateSelectedItem(index);
}
if (Presenter?.Panel != null)

9
src/Avalonia.Controls/Primitives/TabStrip.cs

@ -12,11 +12,8 @@ namespace Avalonia.Controls.Primitives
private static readonly FuncTemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => new WrapPanel { Orientation = Orientation.Horizontal });
private static IMemberSelector s_MemberSelector = new FuncMemberSelector<object, object>(SelectHeader);
static TabStrip()
{
MemberSelectorProperty.OverrideDefaultValue<TabStrip>(s_MemberSelector);
SelectionModeProperty.OverrideDefaultValue<TabStrip>(SelectionMode.AlwaysSelected);
FocusableProperty.OverrideDefaultValue(typeof(TabStrip), false);
ItemsPanelProperty.OverrideDefaultValue<TabStrip>(DefaultPanel);
@ -51,11 +48,5 @@ namespace Avalonia.Controls.Primitives
e.Handled = UpdateSelectionFromEventSource(e.Source);
}
}
private static object SelectHeader(object o)
{
var headered = o as IHeadered;
return (headered != null) ? (headered.Header ?? string.Empty) : o;
}
}
}

35
src/Avalonia.Controls/Templates/FuncMemberSelector.cs

@ -1,35 +0,0 @@
// 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.Controls.Templates
{
/// <summary>
/// Selects a member of an object using a <see cref="Func{TObject, TMember}"/>.
/// </summary>
public class FuncMemberSelector<TObject, TMember> : IMemberSelector
{
private readonly Func<TObject, TMember> _selector;
/// <summary>
/// Initializes a new instance of the <see cref="FuncMemberSelector{TObject, TMember}"/>
/// class.
/// </summary>
/// <param name="selector">The selector.</param>
public FuncMemberSelector(Func<TObject, TMember> selector)
{
this._selector = selector;
}
/// <summary>
/// Selects a member of an object.
/// </summary>
/// <param name="o">The object.</param>
/// <returns>The selected member.</returns>
public object Select(object o)
{
return (o is TObject) ? _selector((TObject)o) : default(TMember);
}
}
}

18
src/Avalonia.Controls/Templates/IMemberSelector.cs

@ -1,18 +0,0 @@
// 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.
namespace Avalonia.Controls.Templates
{
/// <summary>
/// Selects a member of an object.
/// </summary>
public interface IMemberSelector
{
/// <summary>
/// Selects a member of an object.
/// </summary>
/// <param name="o">The object.</param>
/// <returns>The selected member.</returns>
object Select(object o);
}
}

105
src/Avalonia.Controls/TreeView.cs

@ -105,32 +105,21 @@ namespace Avalonia.Controls
get => _selectedItem;
set
{
SetAndRaise(SelectedItemProperty, ref _selectedItem,
(object val, ref object backing, Action<Action> notifyWrapper) =>
{
var old = backing;
backing = val;
notifyWrapper(() =>
RaisePropertyChanged(
SelectedItemProperty,
old,
val));
SetAndRaise(SelectedItemProperty, ref _selectedItem, value);
if (val != null)
{
if (SelectedItems.Count != 1 || SelectedItems[0] != val)
{
_syncingSelectedItems = true;
SelectSingleItem(val);
_syncingSelectedItems = false;
}
}
else if (SelectedItems.Count > 0)
{
SelectedItems.Clear();
}
}, value);
if (value != null)
{
if (SelectedItems.Count != 1 || SelectedItems[0] != value)
{
_syncingSelectedItems = true;
SelectSingleItem(value);
_syncingSelectedItems = false;
}
}
else if (SelectedItems.Count > 0)
{
SelectedItems.Clear();
}
}
}
@ -164,6 +153,48 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Expands the specified <see cref="TreeViewItem"/> all descendent <see cref="TreeViewItem"/>s.
/// </summary>
/// <param name="item">The item to expand.</param>
public void ExpandSubTree(TreeViewItem item)
{
item.IsExpanded = true;
var panel = item.Presenter.Panel;
if (panel != null)
{
foreach (var child in panel.Children)
{
if (child is TreeViewItem treeViewItem)
{
ExpandSubTree(treeViewItem);
}
}
}
}
/// <summary>
/// Selects all items in the <see cref="TreeView"/>.
/// </summary>
/// <remarks>
/// Note that this method only selects nodes currently visible due to their parent nodes
/// being expanded: it does not expand nodes.
/// </remarks>
public void SelectAll()
{
SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items);
}
/// <summary>
/// Deselects all items in the <see cref="TreeView"/>.
/// </summary>
public void UnselectAll()
{
SelectedItems.Clear();
}
/// <summary>
/// Subscribes to the <see cref="SelectedItems"/> CollectionChanged event, if any.
/// </summary>
@ -409,7 +440,7 @@ namespace Avalonia.Controls
if (this.SelectionMode == SelectionMode.Multiple && Match(keymap.SelectAll))
{
SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items);
SelectAll();
e.Handled = true;
}
}
@ -479,7 +510,8 @@ namespace Avalonia.Controls
e.Source,
true,
(e.InputModifiers & InputModifiers.Shift) != 0,
(e.InputModifiers & InputModifiers.Control) != 0);
(e.InputModifiers & InputModifiers.Control) != 0,
e.MouseButton == MouseButton.Right);
}
}
@ -490,11 +522,13 @@ namespace Avalonia.Controls
/// <param name="select">Whether the item should be selected or unselected.</param>
/// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
/// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
/// <param name="rightButton">Whether the event is a right-click.</param>
protected void UpdateSelectionFromContainer(
IControl container,
bool select = true,
bool rangeModifier = false,
bool toggleModifier = false)
bool toggleModifier = false,
bool rightButton = false)
{
var item = ItemContainerGenerator.Index.ItemFromContainer(container);
@ -515,7 +549,14 @@ namespace Avalonia.Controls
var multi = (mode & SelectionMode.Multiple) != 0;
var range = multi && selectedContainer != null && rangeModifier;
if (!toggle && !range)
if (rightButton)
{
if (!SelectedItems.Contains(item))
{
SelectSingleItem(item);
}
}
else if (!toggle && !range)
{
SelectSingleItem(item);
}
@ -684,6 +725,7 @@ namespace Avalonia.Controls
/// <param name="select">Whether the container should be selected or unselected.</param>
/// <param name="rangeModifier">Whether the range modifier is enabled (i.e. shift key).</param>
/// <param name="toggleModifier">Whether the toggle modifier is enabled (i.e. ctrl key).</param>
/// <param name="rightButton">Whether the event is a right-click.</param>
/// <returns>
/// True if the event originated from a container that belongs to the control; otherwise
/// false.
@ -692,13 +734,14 @@ namespace Avalonia.Controls
IInteractive eventSource,
bool select = true,
bool rangeModifier = false,
bool toggleModifier = false)
bool toggleModifier = false,
bool rightButton = false)
{
var container = GetContainerFromEventSource(eventSource);
if (container != null)
{
UpdateSelectionFromContainer(container, select, rangeModifier, toggleModifier);
UpdateSelectionFromContainer(container, select, rangeModifier, toggleModifier, rightButton);
return true;
}

2
src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs

@ -54,7 +54,7 @@ namespace Avalonia.DesignerSupport.Remote
.Bind<IPlatformSettings>().ToConstant(instance)
.Bind<IPlatformThreadingInterface>().ToConstant(threading)
.Bind<IRenderLoop>().ToConstant(new RenderLoop())
.Bind<IRenderTimer>().ToConstant(threading)
.Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))
.Bind<ISystemDialogImpl>().ToSingleton<SystemDialogsStub>()
.Bind<IWindowingPlatform>().ToConstant(instance)
.Bind<IPlatformIconLoader>().ToSingleton<IconLoaderStub>()

37
src/Avalonia.Diagnostics/DevTools.xaml

@ -1,23 +1,24 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Avalonia.Diagnostics.DevTools">
<Grid RowDefinitions="Auto,*,Auto">
<TabStrip SelectedIndex="{Binding SelectedTab, Mode=TwoWay}">
<TabStripItem Content="Logical Tree"/>
<TabStripItem Content="Visual Tree"/>
<TabStripItem Content="Events"/>
</TabStrip>
<Grid RowDefinitions="*,Auto" Margin="4">
<ContentControl Content="{Binding Content}" Grid.Row="1"/>
<StackPanel Spacing="4" Orientation="Horizontal" Grid.Row="2">
<TextBlock>Hold Ctrl+Shift over a control to inspect.</TextBlock>
<Separator Width="8"/>
<TextBlock>Focused:</TextBlock>
<TextBlock Text="{Binding FocusedControl}"/>
<Separator Width="8"/>
<TextBlock>Pointer Over:</TextBlock>
<TextBlock Text="{Binding PointerOverElement}"/>
</StackPanel>
</Grid>
<TabControl Grid.Row="0" Items="{Binding Tools}" SelectedItem="{Binding SelectedTool}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</TabControl.ItemTemplate>
</TabControl>
<StackPanel Grid.Row="1" Spacing="4" Orientation="Horizontal">
<TextBlock>Hold Ctrl+Shift over a control to inspect.</TextBlock>
<Separator Width="8" />
<TextBlock>Focused:</TextBlock>
<TextBlock Text="{Binding FocusedControl}" />
<Separator Width="8" />
<TextBlock>Pointer Over:</TextBlock>
<TextBlock Text="{Binding PointerOverElement}" />
</StackPanel>
</Grid>
</UserControl>

8
src/Avalonia.Diagnostics/DevTools.xaml.cs

@ -1,10 +1,13 @@
// 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.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.Input;
using Avalonia.Input.Raw;
@ -82,7 +85,8 @@ namespace Avalonia.Diagnostics
DataTemplates =
{
new ViewLocator<ViewModelBase>(),
}
},
Title = "Avalonia DevTools"
};
devToolsWindow.Closed += devTools.DevToolsClosed;

2
src/Avalonia.Diagnostics/ViewLocator.cs

@ -31,4 +31,4 @@ namespace Avalonia.Diagnostics
return data is TViewModel;
}
}
}
}

65
src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs

@ -2,7 +2,9 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive.Linq;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Input;
@ -10,21 +12,23 @@ namespace Avalonia.Diagnostics.ViewModels
{
internal class DevToolsViewModel : ViewModelBase
{
private ViewModelBase _content;
private int _selectedTab;
private TreePageViewModel _logicalTree;
private TreePageViewModel _visualTree;
private EventsViewModel _eventsView;
private IDevToolViewModel _selectedTool;
private string _focusedControl;
private string _pointerOverElement;
public DevToolsViewModel(IControl root)
{
_logicalTree = new TreePageViewModel(LogicalTreeNode.Create(root));
_visualTree = new TreePageViewModel(VisualTreeNode.Create(root));
_eventsView = new EventsViewModel(root);
Tools = new ObservableCollection<IDevToolViewModel>
{
new TreePageViewModel(LogicalTreeNode.Create(root), "Logical Tree"),
new TreePageViewModel(VisualTreeNode.Create(root), "Visual Tree"),
new EventsViewModel(root)
};
SelectedTool = Tools.First();
UpdateFocusedControl();
KeyboardDevice.Instance.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(KeyboardDevice.Instance.FocusedElement))
@ -33,58 +37,33 @@ namespace Avalonia.Diagnostics.ViewModels
}
};
SelectedTab = 0;
root.GetObservable(TopLevel.PointerOverElementProperty)
.Subscribe(x => PointerOverElement = x?.GetType().Name);
}
public ViewModelBase Content
public IDevToolViewModel SelectedTool
{
get { return _content; }
private set { RaiseAndSetIfChanged(ref _content, value); }
get => _selectedTool;
set => RaiseAndSetIfChanged(ref _selectedTool, value);
}
public int SelectedTab
{
get { return _selectedTab; }
set
{
_selectedTab = value;
switch (value)
{
case 0:
Content = _logicalTree;
break;
case 1:
Content = _visualTree;
break;
case 2:
Content = _eventsView;
break;
}
RaisePropertyChanged();
}
}
public ObservableCollection<IDevToolViewModel> Tools { get; }
public string FocusedControl
{
get { return _focusedControl; }
private set { RaiseAndSetIfChanged(ref _focusedControl, value); }
get => _focusedControl;
private set => RaiseAndSetIfChanged(ref _focusedControl, value);
}
public string PointerOverElement
{
get { return _pointerOverElement; }
private set { RaiseAndSetIfChanged(ref _pointerOverElement, value); }
get => _pointerOverElement;
private set => RaiseAndSetIfChanged(ref _pointerOverElement, value);
}
public void SelectControl(IControl control)
{
var tree = Content as TreePageViewModel;
if (tree != null)
if (SelectedTool is TreePageViewModel tree)
{
tree.SelectControl(control);
}

13
src/Avalonia.Diagnostics/ViewModels/EventsViewModel.cs

@ -5,8 +5,6 @@ using System;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Windows.Input;
using Avalonia.Controls;
using Avalonia.Data.Converters;
using Avalonia.Interactivity;
@ -14,21 +12,24 @@ using Avalonia.Media;
namespace Avalonia.Diagnostics.ViewModels
{
internal class EventsViewModel : ViewModelBase
internal class EventsViewModel : ViewModelBase, IDevToolViewModel
{
private readonly IControl _root;
private FiredEvent _selectedEvent;
public EventsViewModel(IControl root)
{
this._root = root;
this.Nodes = RoutedEventRegistry.Instance.GetAllRegistered()
_root = root;
Nodes = RoutedEventRegistry.Instance.GetAllRegistered()
.GroupBy(e => e.OwnerType)
.OrderBy(e => e.Key.Name)
.Select(g => new EventOwnerTreeNode(g.Key, g, this))
.ToArray();
}
public string Name => "Events";
public EventTreeNodeBase[] Nodes { get; }
public ObservableCollection<FiredEvent> RecordedEvents { get; } = new ObservableCollection<FiredEvent>();
@ -49,7 +50,7 @@ namespace Avalonia.Diagnostics.ViewModels
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (bool)value ? Brushes.LightGreen : Brushes.Transparent;
return (bool)value ? Brushes.Green : Brushes.Transparent;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)

16
src/Avalonia.Diagnostics/ViewModels/IDevToolViewModel.cs

@ -0,0 +1,16 @@
// 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.
namespace Avalonia.Diagnostics.ViewModels
{
/// <summary>
/// View model interface for tool showing up in DevTools
/// </summary>
public interface IDevToolViewModel
{
/// <summary>
/// Name of a tool.
/// </summary>
string Name { get; }
}
}

7
src/Avalonia.Diagnostics/ViewModels/TreePageViewModel.cs

@ -6,16 +6,19 @@ using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels
{
internal class TreePageViewModel : ViewModelBase
internal class TreePageViewModel : ViewModelBase, IDevToolViewModel
{
private TreeNode _selected;
private ControlDetailsViewModel _details;
public TreePageViewModel(TreeNode[] nodes)
public TreePageViewModel(TreeNode[] nodes, string name)
{
Nodes = nodes;
Name = name;
}
public string Name { get; }
public TreeNode[] Nodes { get; protected set; }
public TreeNode SelectedNode

16
src/Avalonia.Diagnostics/Views/ControlDetailsView.cs

@ -7,7 +7,6 @@ using System.Reactive.Linq;
using Avalonia.Controls;
using Avalonia.Diagnostics.ViewModels;
using Avalonia.Media;
using Avalonia.Styling;
namespace Avalonia.Diagnostics.Views
{
@ -42,16 +41,6 @@ namespace Avalonia.Diagnostics.Views
{
Content = _grid = new SimpleGrid
{
Styles =
{
new Style(x => x.Is<Control>())
{
Setters = new[]
{
new Setter(MarginProperty, new Thickness(2)),
}
},
},
[GridRepeater.TemplateProperty] = pt,
}
};
@ -61,8 +50,11 @@ namespace Avalonia.Diagnostics.Views
{
var property = (PropertyDetails)i;
var margin = new Thickness(2);
yield return new TextBlock
{
Margin = margin,
Text = property.Name,
TextWrapping = TextWrapping.NoWrap,
[!ToolTip.TipProperty] = property.GetObservable<string>(nameof(property.Diagnostic)).ToBinding(),
@ -70,6 +62,7 @@ namespace Avalonia.Diagnostics.Views
yield return new TextBlock
{
Margin = margin,
TextWrapping = TextWrapping.NoWrap,
[!TextBlock.TextProperty] = property.GetObservable<object>(nameof(property.Value))
.Select(v => v?.ToString())
@ -78,6 +71,7 @@ namespace Avalonia.Diagnostics.Views
yield return new TextBlock
{
Margin = margin,
TextWrapping = TextWrapping.NoWrap,
[!TextBlock.TextProperty] = property.GetObservable<string>((nameof(property.Priority))).ToBinding(),
};

5
src/Avalonia.Diagnostics/Views/PropertyChangedExtenions.cs → src/Avalonia.Diagnostics/Views/PropertyChangedExtensions.cs

@ -1,4 +1,7 @@
using System;
// 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.ComponentModel;
using System.Reactive.Linq;
using System.Reflection;

3
src/Avalonia.Diagnostics/Views/TreePage.xaml.cs

@ -1,3 +1,6 @@
// 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.Controls;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Primitives;

3
src/Avalonia.Input/Raw/RawPointerEventArgs.cs

@ -19,7 +19,8 @@ namespace Avalonia.Input.Raw
NonClientLeftButtonDown,
TouchBegin,
TouchUpdate,
TouchEnd
TouchEnd,
TouchCancel
}
/// <summary>

8
src/Avalonia.Input/TouchDevice.cs

@ -61,6 +61,12 @@ namespace Avalonia.Input
pointer.IsPrimary ? MouseButton.Left : MouseButton.None));
}
}
if (args.Type == RawPointerEventType.TouchCancel)
{
_pointers.Remove(args.TouchPointId);
using (pointer)
pointer.Capture(null);
}
if (args.Type == RawPointerEventType.TouchUpdate)
{
@ -68,6 +74,8 @@ namespace Avalonia.Input
target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root,
args.Position, ev.Timestamp, new PointerPointProperties(modifiers), modifiers));
}
}
}

8
src/Avalonia.OpenGL/EglContext.cs

@ -10,7 +10,7 @@ namespace Avalonia.OpenGL
private readonly EglInterface _egl;
private readonly object _lock = new object();
public EglContext(EglDisplay display, EglInterface egl, IntPtr ctx, IntPtr offscreenSurface)
public EglContext(EglDisplay display, EglInterface egl, IntPtr ctx, EglSurface offscreenSurface)
{
_disp = display;
_egl = egl;
@ -19,7 +19,7 @@ namespace Avalonia.OpenGL
}
public IntPtr Context { get; }
public IntPtr OffscreenSurface { get; }
public EglSurface OffscreenSurface { get; }
public IGlDisplay Display => _disp;
public IDisposable Lock()
@ -36,8 +36,8 @@ namespace Avalonia.OpenGL
public void MakeCurrent(EglSurface surface)
{
var surf = surface?.DangerousGetHandle() ?? OffscreenSurface;
if (!_egl.MakeCurrent(_disp.Handle, surf, surf, Context))
var surf = surface ?? OffscreenSurface;
if (!_egl.MakeCurrent(_disp.Handle, surf.DangerousGetHandle(), surf.DangerousGetHandle(), Context))
throw OpenGlException.GetFormattedException("eglMakeCurrent", _egl);
}
}

94
src/Avalonia.OpenGL/EglDisplay.cs

@ -12,49 +12,62 @@ namespace Avalonia.OpenGL
private readonly IntPtr _display;
private readonly IntPtr _config;
private readonly int[] _contextAttributes;
private readonly int _surfaceType;
public IntPtr Handle => _display;
private AngleOptions.PlatformApi? _angleApi;
public EglDisplay(EglInterface egl)
public EglDisplay(EglInterface egl) : this(egl, -1, IntPtr.Zero, null)
{
}
public EglDisplay(EglInterface egl, int platformType, IntPtr platformDisplay, int[] attrs)
{
_egl = egl;
_egl = egl;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
if (platformType == -1 && platformDisplay == IntPtr.Zero)
{
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)
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
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 (_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)
{
_angleApi = platformApi;
break;
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)
{
_angleApi = platformApi;
break;
}
}
if (_display == IntPtr.Zero)
throw new OpenGlException("Unable to create ANGLE display");
}
if (_display == IntPtr.Zero)
throw new OpenGlException("Unable to create ANGLE display");
_display = _egl.GetDisplay(IntPtr.Zero);
}
else
{
if (_egl.GetPlatformDisplayEXT == null)
throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl");
_display = _egl.GetPlatformDisplayEXT(platformType, platformDisplay, attrs);
}
if (_display == IntPtr.Zero)
_display = _egl.GetDisplay(IntPtr.Zero);
if (_display == IntPtr.Zero)
throw OpenGlException.GetFormattedException("eglGetDisplay", _egl);
@ -85,16 +98,14 @@ namespace Avalonia.OpenGL
{
if (!_egl.BindApi(cfg.Api))
continue;
foreach(var surfaceType in new[]{EGL_PBUFFER_BIT|EGL_WINDOW_BIT, EGL_WINDOW_BIT})
foreach(var stencilSize in new[]{8, 1, 0})
foreach (var depthSize in new []{8, 1, 0})
{
var attribs = new[]
{
EGL_SURFACE_TYPE, EGL_PBUFFER_BIT,
EGL_SURFACE_TYPE, surfaceType,
EGL_RENDERABLE_TYPE, cfg.RenderableTypeBit,
EGL_RED_SIZE, 8,
EGL_GREEN_SIZE, 8,
EGL_BLUE_SIZE, 8,
@ -108,6 +119,7 @@ namespace Avalonia.OpenGL
if (numConfigs == 0)
continue;
_contextAttributes = cfg.Attributes;
_surfaceType = surfaceType;
Type = cfg.Type;
}
}
@ -126,8 +138,10 @@ namespace Avalonia.OpenGL
public GlDisplayType Type { get; }
public GlInterface GlInterface { get; }
public EglInterface EglInterface => _egl;
public IGlContext CreateContext(IGlContext share)
public EglContext CreateContext(IGlContext share)
{
if((_surfaceType|EGL_PBUFFER_BIT) == 0)
throw new InvalidOperationException("Platform doesn't support PBUFFER surfaces");
var shareCtx = (EglContext)share;
var ctx = _egl.CreateContext(_display, _config, shareCtx?.Context ?? IntPtr.Zero, _contextAttributes);
if (ctx == IntPtr.Zero)
@ -140,7 +154,17 @@ namespace Avalonia.OpenGL
});
if (surf == IntPtr.Zero)
throw OpenGlException.GetFormattedException("eglCreatePBufferSurface", _egl);
var rv = new EglContext(this, _egl, ctx, surf);
var rv = new EglContext(this, _egl, ctx, new EglSurface(this, _egl, surf));
rv.MakeCurrent(null);
return rv;
}
public EglContext CreateContext(EglContext share, EglSurface offscreenSurface)
{
var ctx = _egl.CreateContext(_display, _config, share?.Context ?? IntPtr.Zero, _contextAttributes);
if (ctx == IntPtr.Zero)
throw OpenGlException.GetFormattedException("eglCreateContext", _egl);
var rv = new EglContext(this, _egl, ctx, offscreenSurface);
rv.MakeCurrent(null);
return rv;
}

5
src/Avalonia.OpenGL/EglInterface.cs

@ -10,6 +10,11 @@ namespace Avalonia.OpenGL
public EglInterface() : base(Load())
{
}
public EglInterface(Func<Utf8Buffer,IntPtr> getProcAddress) : base(getProcAddress)
{
}
public EglInterface(string library) : base(Load(library))

2
src/Avalonia.OpenGL/GlInterface.cs

@ -39,7 +39,7 @@ namespace Avalonia.OpenGL
[GlEntryPoint("glClearStencil")]
public GlClearStencil ClearStencil { get; }
public delegate void GlClearColor(int r, int g, int b, int a);
public delegate void GlClearColor(float r, float g, float b, float a);
[GlEntryPoint("glClearColor")]
public GlClearColor ClearColor { get; }

6
src/Avalonia.Styling/Controls/NameScopeLocator.cs

@ -15,10 +15,8 @@ namespace Avalonia.Controls
/// <summary>
/// Tracks a named control relative to another control.
/// </summary>
/// <param name="relativeTo">
/// The control relative from which the other control should be found.
/// </param>
/// <param name="name">The name of the control to find.</param>
/// <param name="scope">The scope relative from which the object should be resolved.</param>
/// <param name="name">The name of the object to find.</param>
public static IObservable<object> Track(INameScope scope, string name)
{
return new NeverEndingSynchronousCompletionAsyncResultObservable<object>(scope.FindAsync(name));

1
src/Avalonia.Themes.Default/AutoCompleteBox.xaml

@ -27,7 +27,6 @@
Background="{TemplateBinding Background}"
Foreground="{TemplateBinding Foreground}"
ItemTemplate="{TemplateBinding ItemTemplate}"
MemberSelector="{TemplateBinding ValueMemberSelector}"
ScrollViewer.HorizontalScrollBarVisibility="Auto"
ScrollViewer.VerticalScrollBarVisibility="Auto" />
</Border>

3
src/Avalonia.Themes.Default/Carousel.xaml

@ -8,10 +8,9 @@
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
Margin="{TemplateBinding Padding}"
MemberSelector="{TemplateBinding MemberSelector}"
SelectedIndex="{TemplateBinding SelectedIndex}"
PageTransition="{TemplateBinding PageTransition}"/>
</Border>
</ControlTemplate>
</Setter>
</Style>
</Style>

1
src/Avalonia.Themes.Default/ComboBox.xaml

@ -45,7 +45,6 @@
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
MemberSelector="{TemplateBinding MemberSelector}"
VirtualizationMode="{TemplateBinding VirtualizationMode}"
/>
</ScrollViewer>

2
src/Avalonia.Themes.Default/DataValidationErrors.xaml

@ -29,7 +29,7 @@
</Style>
</Canvas.Styles>
<ToolTip.Tip>
<ItemsControl Items="{Binding}" MemberSelector="Message"/>
<ItemsControl Items="{Binding}"/>
</ToolTip.Tip>
<Path Data="M14,7 A7,7 0 0,0 0,7 M0,7 A7,7 0 1,0 14,7 M7,3l0,5 M7,9l0,2" Stroke="{DynamicResource ErrorBrush}" StrokeThickness="2"/>
</Canvas>

5
src/Avalonia.Themes.Default/ItemsControl.xaml

@ -4,8 +4,7 @@
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
MemberSelector="{TemplateBinding MemberSelector}"/>
ItemTemplate="{TemplateBinding ItemTemplate}"/>
</ControlTemplate>
</Setter>
</Style>
</Style>

3
src/Avalonia.Themes.Default/ListBox.xaml

@ -18,10 +18,9 @@
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
Margin="{TemplateBinding Padding}"
MemberSelector="{TemplateBinding MemberSelector}"
VirtualizationMode="{TemplateBinding VirtualizationMode}"/>
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter>
</Style>
</Style>

6
src/Avalonia.Themes.Default/MenuItem.xaml

@ -56,8 +56,7 @@
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
Margin="2"
MemberSelector="{TemplateBinding MemberSelector}"/>
Margin="2"/>
<Rectangle Name="iconSeparator"
Fill="{DynamicResource ThemeControlMidBrush}"
HorizontalAlignment="Left"
@ -114,8 +113,7 @@
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
Margin="2"
MemberSelector="{TemplateBinding MemberSelector}"/>
Margin="2"/>
<Rectangle Name="iconSeparator"
Fill="{DynamicResource ThemeControlMidBrush}"
HorizontalAlignment="Left"

3
src/Avalonia.Themes.Default/TabControl.xaml

@ -14,8 +14,7 @@
Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
MemberSelector="{TemplateBinding MemberSelector}" >
ItemTemplate="{TemplateBinding ItemTemplate}">
</ItemsPresenter>
<ContentPresenter
Name="PART_SelectedContentHost"

3
src/Avalonia.Themes.Default/TabStrip.xaml

@ -3,7 +3,6 @@
<Setter Property="Template">
<ControlTemplate>
<ItemsPresenter Name="PART_ItemsPresenter"
MemberSelector="{TemplateBinding MemberSelector}"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"/>
@ -18,4 +17,4 @@
<Style Selector="TabStrip > TabStripItem">
<Setter Property="Margin" Value="16"/>
</Style>
</Styles>
</Styles>

3
src/Avalonia.Themes.Default/TreeView.xaml

@ -15,8 +15,7 @@
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
Margin="{TemplateBinding Padding}"
MemberSelector="{TemplateBinding MemberSelector}"/>
Margin="{TemplateBinding Padding}"/>
</ScrollViewer>
</Border>
</ControlTemplate>

3
src/Avalonia.Themes.Default/TreeViewItem.xaml

@ -32,8 +32,7 @@
<ItemsPresenter Name="PART_ItemsPresenter"
IsVisible="{TemplateBinding IsExpanded}"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
MemberSelector="{TemplateBinding MemberSelector}"/>
ItemsPanel="{TemplateBinding ItemsPanel}"/>
</StackPanel>
</ControlTemplate>
</Setter>

5
src/Avalonia.Visuals/Rendering/RenderLayers.cs

@ -8,8 +8,8 @@ namespace Avalonia.Rendering
{
public class RenderLayers : IEnumerable<RenderLayer>
{
private List<RenderLayer> _inner = new List<RenderLayer>();
private Dictionary<IVisual, RenderLayer> _index = new Dictionary<IVisual, RenderLayer>();
private readonly List<RenderLayer> _inner = new List<RenderLayer>();
private readonly Dictionary<IVisual, RenderLayer> _index = new Dictionary<IVisual, RenderLayer>();
public int Count => _inner.Count;
public RenderLayer this[IVisual layerRoot] => _index[layerRoot];
@ -56,6 +56,7 @@ namespace Avalonia.Rendering
}
_index.Clear();
_inner.Clear();
}
public bool TryGetValue(IVisual layerRoot, out RenderLayer value)

88
src/Linux/Avalonia.LinuxFramebuffer/EvDevDevice.cs

@ -1,88 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
namespace Avalonia.LinuxFramebuffer
{
unsafe class EvDevDevice
{
private static readonly Lazy<List<EvDevDevice>> AllMouseDevices = new Lazy<List<EvDevDevice>>(()
=> OpenMouseDevices());
private static List<EvDevDevice> OpenMouseDevices()
{
var rv = new List<EvDevDevice>();
foreach (var dev in Directory.GetFiles("/dev/input", "event*").Select(Open))
{
if (!dev.IsMouse)
NativeUnsafeMethods.close(dev.Fd);
else
rv.Add(dev);
}
return rv;
}
public static IReadOnlyList<EvDevDevice> MouseDevices => AllMouseDevices.Value;
public int Fd { get; }
private IntPtr _dev;
public string Name { get; }
public List<EvType> EventTypes { get; private set; } = new List<EvType>();
public input_absinfo? AbsX { get; }
public input_absinfo? AbsY { get; }
public EvDevDevice(int fd, IntPtr dev)
{
Fd = fd;
_dev = dev;
Name = Marshal.PtrToStringAnsi(NativeUnsafeMethods.libevdev_get_name(_dev));
foreach (EvType type in Enum.GetValues(typeof(EvType)))
{
if (NativeUnsafeMethods.libevdev_has_event_type(dev, type) != 0)
EventTypes.Add(type);
}
var ptr = NativeUnsafeMethods.libevdev_get_abs_info(dev, (int) AbsAxis.ABS_X);
if (ptr != null)
AbsX = *ptr;
ptr = NativeUnsafeMethods.libevdev_get_abs_info(dev, (int)AbsAxis.ABS_Y);
if (ptr != null)
AbsY = *ptr;
}
public input_event? NextEvent()
{
input_event ev;
if (NativeUnsafeMethods.libevdev_next_event(_dev, 2, out ev) == 0)
return ev;
return null;
}
public bool IsMouse => EventTypes.Contains(EvType.EV_REL);
public static EvDevDevice Open(string device)
{
var fd = NativeUnsafeMethods.open(device, 2048, 0);
if (fd <= 0)
throw new Exception($"Unable to open {device} code {Marshal.GetLastWin32Error()}");
IntPtr dev;
var rc = NativeUnsafeMethods.libevdev_new_from_fd(fd, out dev);
if (rc < 0)
{
NativeUnsafeMethods.close(fd);
throw new Exception($"Unable to initialize evdev for {device} code {Marshal.GetLastWin32Error()}");
}
return new EvDevDevice(fd, dev);
}
}
public class EvDevAxisInfo
{
public int Minimum { get; set; }
public int Maximum { get; set; }
}
}

38
src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs

@ -2,30 +2,35 @@
using System.Collections.Generic;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.LinuxFramebuffer.Input;
using Avalonia.LinuxFramebuffer.Output;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Threading;
namespace Avalonia.LinuxFramebuffer
{
class FramebufferToplevelImpl : IEmbeddableWindowImpl
class FramebufferToplevelImpl : IEmbeddableWindowImpl, IScreenInfoProvider
{
private readonly LinuxFramebuffer _fb;
private readonly IOutputBackend _outputBackend;
private readonly IInputBackend _inputBackend;
private bool _renderQueued;
public IInputRoot InputRoot { get; private set; }
public FramebufferToplevelImpl(LinuxFramebuffer fb)
public FramebufferToplevelImpl(IOutputBackend outputBackend, IInputBackend inputBackend)
{
_fb = fb;
_outputBackend = outputBackend;
_inputBackend = inputBackend;
Invalidate(default(Rect));
var mice = new Mice(this, ClientSize.Width, ClientSize.Height);
mice.Start();
mice.Event += e => Input?.Invoke(e);
_inputBackend.Initialize(this, e => Input?.Invoke(e));
}
public IRenderer CreateRenderer(IRenderRoot root)
{
return new ImmediateRenderer(root);
return new DeferredRenderer(root, AvaloniaLocator.Current.GetService<IRenderLoop>())
{
};
}
public void Dispose()
@ -36,19 +41,12 @@ namespace Avalonia.LinuxFramebuffer
public void Invalidate(Rect rect)
{
if(_renderQueued)
return;
_renderQueued = true;
Dispatcher.UIThread.Post(() =>
{
Paint?.Invoke(new Rect(default(Point), ClientSize));
_renderQueued = false;
});
}
public void SetInputRoot(IInputRoot inputRoot)
{
InputRoot = inputRoot;
_inputBackend.SetInputRoot(inputRoot);
}
public Point PointToClient(PixelPoint p) => p.ToPoint(1);
@ -59,10 +57,10 @@ namespace Avalonia.LinuxFramebuffer
{
}
public Size ClientSize => _fb.PixelSize;
public IMouseDevice MouseDevice => LinuxFramebufferPlatform.MouseDevice;
public Size ClientSize => ScaledSize;
public IMouseDevice MouseDevice => new MouseDevice();
public double Scaling => 1;
public IEnumerable<object> Surfaces => new object[] {_fb};
public IEnumerable<object> Surfaces => new object[] {_outputBackend};
public Action<RawInputEventArgs> Input { get; set; }
public Action<Rect> Paint { get; set; }
public Action<Size> Resized { get; set; }
@ -73,5 +71,7 @@ namespace Avalonia.LinuxFramebuffer
add {}
remove {}
}
public Size ScaledSize => _outputBackend.PixelSize.ToSize(Scaling);
}
}

12
src/Linux/Avalonia.LinuxFramebuffer/Input/IInputBackend.cs

@ -0,0 +1,12 @@
using System;
using Avalonia.Input;
using Avalonia.Input.Raw;
namespace Avalonia.LinuxFramebuffer.Input
{
public interface IInputBackend
{
void Initialize(IScreenInfoProvider info, Action<RawInputEventArgs> onInput);
void SetInputRoot(IInputRoot root);
}
}

7
src/Linux/Avalonia.LinuxFramebuffer/Input/IScreenInfoProvider.cs

@ -0,0 +1,7 @@
namespace Avalonia.LinuxFramebuffer.Input
{
public interface IScreenInfoProvider
{
Size ScaledSize { get; }
}
}

183
src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs

@ -0,0 +1,183 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Threading;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Threading;
using static Avalonia.LinuxFramebuffer.Input.LibInput.LibInputNativeUnsafeMethods;
namespace Avalonia.LinuxFramebuffer.Input.LibInput
{
public class LibInputBackend : IInputBackend
{
private IScreenInfoProvider _screen;
private IInputRoot _inputRoot;
private readonly Queue<Action> _inputThreadActions = new Queue<Action>();
private TouchDevice _touch = new TouchDevice();
private MouseDevice _mouse = new MouseDevice();
private Point _mousePosition;
private readonly Queue<RawInputEventArgs> _inputQueue = new Queue<RawInputEventArgs>();
private Action<RawInputEventArgs> _onInput;
private Dictionary<int, Point> _pointers = new Dictionary<int, Point>();
public LibInputBackend()
{
var ctx = libinput_path_create_context();
new Thread(()=>InputThread(ctx)).Start();
}
private unsafe void InputThread(IntPtr ctx)
{
var fd = libinput_get_fd(ctx);
var timeval = stackalloc IntPtr[2];
foreach (var f in Directory.GetFiles("/dev/input", "event*"))
libinput_path_add_device(ctx, f);
while (true)
{
IntPtr ev;
libinput_dispatch(ctx);
while ((ev = libinput_get_event(ctx)) != IntPtr.Zero)
{
var type = libinput_event_get_type(ev);
if (type >= LibInputEventType.LIBINPUT_EVENT_TOUCH_DOWN &&
type <= LibInputEventType.LIBINPUT_EVENT_TOUCH_CANCEL)
HandleTouch(ev, type);
if (type >= LibInputEventType.LIBINPUT_EVENT_POINTER_MOTION
&& type <= LibInputEventType.LIBINPUT_EVENT_POINTER_AXIS)
HandlePointer(ev, type);
libinput_event_destroy(ev);
libinput_dispatch(ctx);
}
pollfd pfd = new pollfd {fd = fd, events = 1};
NativeUnsafeMethods.poll(&pfd, new IntPtr(1), 10);
}
}
private void ScheduleInput(RawInputEventArgs ev)
{
lock (_inputQueue)
{
_inputQueue.Enqueue(ev);
if (_inputQueue.Count == 1)
{
Dispatcher.UIThread.Post(() =>
{
while (true)
{
Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1);
RawInputEventArgs dequeuedEvent = null;
lock(_inputQueue)
if (_inputQueue.Count != 0)
dequeuedEvent = _inputQueue.Dequeue();
if (dequeuedEvent == null)
return;
_onInput?.Invoke(dequeuedEvent);
}
}, DispatcherPriority.Input);
}
}
}
private void HandleTouch(IntPtr ev, LibInputEventType type)
{
var tev = libinput_event_get_touch_event(ev);
if(tev == IntPtr.Zero)
return;
if (type < LibInputEventType.LIBINPUT_EVENT_TOUCH_FRAME)
{
var info = _screen.ScaledSize;
var slot = libinput_event_touch_get_slot(tev);
Point pt;
if (type == LibInputEventType.LIBINPUT_EVENT_TOUCH_DOWN
|| type == LibInputEventType.LIBINPUT_EVENT_TOUCH_MOTION)
{
var x = libinput_event_touch_get_x_transformed(tev, (int)info.Width);
var y = libinput_event_touch_get_y_transformed(tev, (int)info.Height);
pt = new Point(x, y);
_pointers[slot] = pt;
}
else
{
_pointers.TryGetValue(slot, out pt);
_pointers.Remove(slot);
}
var ts = libinput_event_touch_get_time_usec(tev) / 1000;
if (_inputRoot == null)
return;
ScheduleInput(new RawTouchEventArgs(_touch, ts,
_inputRoot,
type == LibInputEventType.LIBINPUT_EVENT_TOUCH_DOWN ? RawPointerEventType.TouchBegin
: type == LibInputEventType.LIBINPUT_EVENT_TOUCH_UP ? RawPointerEventType.TouchEnd
: type == LibInputEventType.LIBINPUT_EVENT_TOUCH_MOTION ? RawPointerEventType.TouchUpdate
: RawPointerEventType.TouchCancel,
pt, InputModifiers.None, slot));
}
}
private void HandlePointer(IntPtr ev, LibInputEventType type)
{
//TODO: support input modifiers
var pev = libinput_event_get_pointer_event(ev);
var info = _screen.ScaledSize;
var ts = libinput_event_pointer_get_time_usec(pev) / 1000;
if (type == LibInputEventType.LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE)
{
_mousePosition = new Point(libinput_event_pointer_get_absolute_x_transformed(pev, (int)info.Width),
libinput_event_pointer_get_absolute_y_transformed(pev, (int)info.Height));
ScheduleInput(new RawPointerEventArgs(_mouse, ts, _inputRoot, RawPointerEventType.Move, _mousePosition,
InputModifiers.None));
}
else if (type == LibInputEventType.LIBINPUT_EVENT_POINTER_BUTTON)
{
var button = (EvKey)libinput_event_pointer_get_button(pev);
var buttonState = libinput_event_pointer_get_button_state(pev);
var evnt = button == EvKey.BTN_LEFT ?
(buttonState == 1 ? RawPointerEventType.LeftButtonDown : RawPointerEventType.LeftButtonUp) :
button == EvKey.BTN_MIDDLE ?
(buttonState == 1 ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.MiddleButtonUp) :
button == EvKey.BTN_RIGHT ?
(buttonState == 1 ?
RawPointerEventType.RightButtonDown :
RawPointerEventType.RightButtonUp) :
(RawPointerEventType)(-1);
if (evnt == (RawPointerEventType)(-1))
return;
ScheduleInput(
new RawPointerEventArgs(_mouse, ts, _inputRoot, evnt, _mousePosition, InputModifiers.None));
}
}
public void Initialize(IScreenInfoProvider screen, Action<RawInputEventArgs> onInput)
{
_screen = screen;
_onInput = onInput;
}
public void SetInputRoot(IInputRoot root)
{
_inputRoot = root;
}
}
}

139
src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputNativeUnsafeMethods.cs

@ -0,0 +1,139 @@
using System;
using System.Runtime.InteropServices;
namespace Avalonia.LinuxFramebuffer.Input.LibInput
{
unsafe class LibInputNativeUnsafeMethods
{
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate int OpenRestrictedCallbackDelegate(IntPtr path, int flags, IntPtr userData);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void CloseRestrictedCallbackDelegate(int fd, IntPtr userData);
static int OpenRestricted(IntPtr path, int flags, IntPtr userData)
{
var fd = NativeUnsafeMethods.open(Marshal.PtrToStringAnsi(path), flags, 0);
if (fd == -1)
return -Marshal.GetLastWin32Error();
return fd;
}
static void CloseRestricted(int fd, IntPtr userData)
{
NativeUnsafeMethods.close(fd);
}
private static readonly IntPtr* s_Interface;
static LibInputNativeUnsafeMethods()
{
s_Interface = (IntPtr*)Marshal.AllocHGlobal(IntPtr.Size * 2);
IntPtr Convert<TDelegate>(TDelegate del)
{
GCHandle.Alloc(del);
return Marshal.GetFunctionPointerForDelegate(del);
}
s_Interface[0] = Convert<OpenRestrictedCallbackDelegate>(OpenRestricted);
s_Interface[1] = Convert<CloseRestrictedCallbackDelegate>(CloseRestricted);
}
private const string LibInput = "libinput.so.10";
[DllImport(LibInput)]
public extern static IntPtr libinput_path_create_context(IntPtr* iface, IntPtr userData);
public static IntPtr libinput_path_create_context() =>
libinput_path_create_context(s_Interface, IntPtr.Zero);
[DllImport(LibInput)]
public extern static IntPtr libinput_path_add_device(IntPtr ctx, [MarshalAs(UnmanagedType.LPStr)] string path);
[DllImport(LibInput)]
public extern static IntPtr libinput_path_remove_device(IntPtr device);
[DllImport(LibInput)]
public extern static int libinput_get_fd(IntPtr ctx);
[DllImport(LibInput)]
public extern static void libinput_dispatch(IntPtr ctx);
[DllImport(LibInput)]
public extern static IntPtr libinput_get_event(IntPtr ctx);
[DllImport(LibInput)]
public extern static LibInputEventType libinput_event_get_type(IntPtr ev);
public enum LibInputEventType
{
LIBINPUT_EVENT_NONE = 0,
LIBINPUT_EVENT_DEVICE_ADDED,
LIBINPUT_EVENT_DEVICE_REMOVED,
LIBINPUT_EVENT_KEYBOARD_KEY = 300,
LIBINPUT_EVENT_POINTER_MOTION = 400,
LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE,
LIBINPUT_EVENT_POINTER_BUTTON,
LIBINPUT_EVENT_POINTER_AXIS,
LIBINPUT_EVENT_TOUCH_DOWN = 500,
LIBINPUT_EVENT_TOUCH_UP,
LIBINPUT_EVENT_TOUCH_MOTION,
LIBINPUT_EVENT_TOUCH_CANCEL,
LIBINPUT_EVENT_TOUCH_FRAME,
LIBINPUT_EVENT_TABLET_TOOL_AXIS = 600,
LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY,
LIBINPUT_EVENT_TABLET_TOOL_TIP,
LIBINPUT_EVENT_TABLET_TOOL_BUTTON,
LIBINPUT_EVENT_TABLET_PAD_BUTTON = 700,
LIBINPUT_EVENT_TABLET_PAD_RING,
LIBINPUT_EVENT_TABLET_PAD_STRIP,
LIBINPUT_EVENT_GESTURE_SWIPE_BEGIN = 800,
LIBINPUT_EVENT_GESTURE_SWIPE_UPDATE,
LIBINPUT_EVENT_GESTURE_SWIPE_END,
LIBINPUT_EVENT_GESTURE_PINCH_BEGIN,
LIBINPUT_EVENT_GESTURE_PINCH_UPDATE,
LIBINPUT_EVENT_GESTURE_PINCH_END,
LIBINPUT_EVENT_SWITCH_TOGGLE = 900,
}
[DllImport(LibInput)]
public extern static void libinput_event_destroy(IntPtr ev);
[DllImport(LibInput)]
public extern static IntPtr libinput_event_get_touch_event(IntPtr ev);
[DllImport(LibInput)]
public extern static int libinput_event_touch_get_slot(IntPtr ev);
[DllImport(LibInput)]
public extern static ulong libinput_event_touch_get_time_usec(IntPtr ev);
[DllImport(LibInput)]
public extern static double libinput_event_touch_get_x_transformed(IntPtr ev, int width);
[DllImport(LibInput)]
public extern static double libinput_event_touch_get_y_transformed(IntPtr ev, int height);
[DllImport(LibInput)]
public extern static IntPtr libinput_event_get_pointer_event(IntPtr ev);
[DllImport(LibInput)]
public extern static ulong libinput_event_pointer_get_time_usec(IntPtr ev);
[DllImport(LibInput)]
public extern static double libinput_event_pointer_get_absolute_x_transformed(IntPtr ev, int width);
[DllImport(LibInput)]
public extern static double libinput_event_pointer_get_absolute_y_transformed(IntPtr ev, int height);
[DllImport(LibInput)]
public extern static int libinput_event_pointer_get_button(IntPtr ev);
[DllImport(LibInput)]
public extern static int libinput_event_pointer_get_button_state(IntPtr ev);
}
}

46
src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs

@ -8,6 +8,9 @@ using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.LinuxFramebuffer;
using Avalonia.LinuxFramebuffer.Input.LibInput;
using Avalonia.LinuxFramebuffer.Output;
using Avalonia.OpenGL;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Threading;
@ -16,34 +19,37 @@ namespace Avalonia.LinuxFramebuffer
{
class LinuxFramebufferPlatform
{
LinuxFramebuffer _fb;
public static KeyboardDevice KeyboardDevice = new KeyboardDevice();
public static MouseDevice MouseDevice = new MouseDevice();
IOutputBackend _fb;
private static readonly Stopwatch St = Stopwatch.StartNew();
internal static uint Timestamp => (uint)St.ElapsedTicks;
public static InternalPlatformThreadingInterface Threading;
LinuxFramebufferPlatform(string fbdev = null)
LinuxFramebufferPlatform(IOutputBackend backend)
{
_fb = new LinuxFramebuffer(fbdev);
_fb = backend;
}
void Initialize()
{
Threading = new InternalPlatformThreadingInterface();
if (_fb is IWindowingPlatformGlFeature glFeature)
AvaloniaLocator.CurrentMutable.Bind<IWindowingPlatformGlFeature>().ToConstant(glFeature);
AvaloniaLocator.CurrentMutable
.Bind<IPlatformThreadingInterface>().ToConstant(Threading)
.Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))
.Bind<IRenderLoop>().ToConstant(new RenderLoop())
.Bind<IStandardCursorFactory>().ToTransient<CursorFactoryStub>()
.Bind<IKeyboardDevice>().ToConstant(KeyboardDevice)
.Bind<IKeyboardDevice>().ToConstant(new KeyboardDevice())
.Bind<IPlatformSettings>().ToSingleton<PlatformSettings>()
.Bind<IPlatformThreadingInterface>().ToConstant(Threading)
.Bind<IRenderLoop>().ToConstant(new RenderLoop())
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>()
.Bind<IRenderTimer>().ToConstant(Threading);
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>();
}
internal static LinuxFramebufferLifetime Initialize<T>(T builder, string fbdev = null) where T : AppBuilderBase<T>, new()
internal static LinuxFramebufferLifetime Initialize<T>(T builder, IOutputBackend outputBackend) where T : AppBuilderBase<T>, new()
{
var platform = new LinuxFramebufferPlatform(fbdev);
var platform = new LinuxFramebufferPlatform(outputBackend);
builder.UseSkia().UseWindowingSubsystem(platform.Initialize, "fbdev");
return new LinuxFramebufferLifetime(platform._fb);
}
@ -51,12 +57,12 @@ namespace Avalonia.LinuxFramebuffer
class LinuxFramebufferLifetime : IControlledApplicationLifetime, ISingleViewApplicationLifetime
{
private readonly LinuxFramebuffer _fb;
private readonly IOutputBackend _fb;
private TopLevel _topLevel;
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
public CancellationToken Token => _cts.Token;
public LinuxFramebufferLifetime(LinuxFramebuffer fb)
public LinuxFramebufferLifetime(IOutputBackend fb)
{
_fb = fb;
}
@ -69,10 +75,12 @@ namespace Avalonia.LinuxFramebuffer
if (_topLevel == null)
{
var tl = new EmbeddableControlRoot(new FramebufferToplevelImpl(_fb));
var tl = new EmbeddableControlRoot(new FramebufferToplevelImpl(_fb, new LibInputBackend()));
tl.Prepare();
_topLevel = tl;
_topLevel.Renderer.Start();
}
_topLevel.Content = value;
}
}
@ -99,10 +107,16 @@ namespace Avalonia.LinuxFramebuffer
public static class LinuxFramebufferPlatformExtensions
{
public static int StartLinuxFramebuffer<T>(this T builder, string[] args, string fbdev = null)
public static int StartLinuxFbDev<T>(this T builder, string[] args, string fbdev = null)
where T : AppBuilderBase<T>, new() => StartLinuxDirect(builder, args, new FbdevOutput(fbdev));
public static int StartLinuxDrm<T>(this T builder, string[] args, string card = null)
where T : AppBuilderBase<T>, new() => StartLinuxDirect(builder, args, new DrmOutput(card));
public static int StartLinuxDirect<T>(this T builder, string[] args, IOutputBackend backend)
where T : AppBuilderBase<T>, new()
{
var lifetime = LinuxFramebufferPlatform.Initialize(builder, fbdev);
var lifetime = LinuxFramebufferPlatform.Initialize(builder, backend);
builder.Instance.ApplicationLifetime = lifetime;
builder.SetupWithoutStarting();
lifetime.Start(args);

117
src/Linux/Avalonia.LinuxFramebuffer/Mice.cs

@ -1,117 +0,0 @@
using System;
using System.Linq;
using System.Threading;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Platform;
namespace Avalonia.LinuxFramebuffer
{
unsafe class Mice
{
private readonly FramebufferToplevelImpl _topLevel;
private readonly double _width;
private readonly double _height;
private double _x;
private double _y;
public event Action<RawInputEventArgs> Event;
public Mice(FramebufferToplevelImpl topLevel, double width, double height)
{
_topLevel = topLevel;
_width = width;
_height = height;
}
public void Start() => ThreadPool.UnsafeQueueUserWorkItem(_ => Worker(), null);
private void Worker()
{
var mouseDevices = EvDevDevice.MouseDevices.Where(d => d.IsMouse).ToList();
if (mouseDevices.Count == 0)
return;
var are = new AutoResetEvent(false);
while (true)
{
try
{
var rfds = new fd_set {count = mouseDevices.Count};
for (int c = 0; c < mouseDevices.Count; c++)
rfds.fds[c] = mouseDevices[c].Fd;
IntPtr* timeval = stackalloc IntPtr[2];
timeval[0] = new IntPtr(0);
timeval[1] = new IntPtr(100);
are.WaitOne(30);
foreach (var dev in mouseDevices)
{
while(true)
{
var ev = dev.NextEvent();
if (!ev.HasValue)
break;
LinuxFramebufferPlatform.Threading.Send(() => ProcessEvent(dev, ev.Value));
}
}
}
catch (Exception e)
{
Console.Error.WriteLine(e.ToString());
}
}
}
static double TranslateAxis(input_absinfo axis, int value, double max)
{
return (value - axis.minimum) / (double) (axis.maximum - axis.minimum) * max;
}
private void ProcessEvent(EvDevDevice device, input_event ev)
{
if (ev.type == (short)EvType.EV_REL)
{
if (ev.code == (short) AxisEventCode.REL_X)
_x = Math.Min(_width, Math.Max(0, _x + ev.value));
else if (ev.code == (short) AxisEventCode.REL_Y)
_y = Math.Min(_height, Math.Max(0, _y + ev.value));
else
return;
Event?.Invoke(new RawPointerEventArgs(LinuxFramebufferPlatform.MouseDevice,
LinuxFramebufferPlatform.Timestamp,
_topLevel.InputRoot, RawPointerEventType.Move, new Point(_x, _y),
InputModifiers.None));
}
if (ev.type ==(int) EvType.EV_ABS)
{
if (ev.code == (short) AbsAxis.ABS_X && device.AbsX.HasValue)
_x = TranslateAxis(device.AbsX.Value, ev.value, _width);
else if (ev.code == (short) AbsAxis.ABS_Y && device.AbsY.HasValue)
_y = TranslateAxis(device.AbsY.Value, ev.value, _height);
else
return;
Event?.Invoke(new RawPointerEventArgs(LinuxFramebufferPlatform.MouseDevice,
LinuxFramebufferPlatform.Timestamp,
_topLevel.InputRoot, RawPointerEventType.Move, new Point(_x, _y),
InputModifiers.None));
}
if (ev.type == (short) EvType.EV_KEY)
{
RawPointerEventType? type = null;
if (ev.code == (ushort) EvKey.BTN_LEFT)
type = ev.value == 1 ? RawPointerEventType.LeftButtonDown : RawPointerEventType.LeftButtonUp;
if (ev.code == (ushort)EvKey.BTN_RIGHT)
type = ev.value == 1 ? RawPointerEventType.RightButtonDown : RawPointerEventType.RightButtonUp;
if (ev.code == (ushort) EvKey.BTN_MIDDLE)
type = ev.value == 1 ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.MiddleButtonUp;
if (!type.HasValue)
return;
Event?.Invoke(new RawPointerEventArgs(LinuxFramebufferPlatform.MouseDevice,
LinuxFramebufferPlatform.Timestamp,
_topLevel.InputRoot, type.Value, new Point(_x, _y), default(InputModifiers)));
}
}
}
}

13
src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs

@ -33,6 +33,10 @@ namespace Avalonia.LinuxFramebuffer
[DllImport("libc", EntryPoint = "select", SetLastError = true)]
public static extern int select(int nfds, void* rfds, void* wfds, void* exfds, IntPtr* timevals);
[DllImport("libc", EntryPoint = "poll", SetLastError = true)]
public static extern int poll(pollfd* fds, IntPtr nfds, int timeout);
[DllImport("libevdev.so.2", EntryPoint = "libevdev_new_from_fd", SetLastError = true)]
public static extern int libevdev_new_from_fd(int fd, out IntPtr dev);
@ -48,6 +52,13 @@ namespace Avalonia.LinuxFramebuffer
public static extern input_absinfo* libevdev_get_abs_info(IntPtr dev, int code);
}
[StructLayout(LayoutKind.Sequential)]
struct pollfd {
public int fd; /* file descriptor */
public short events; /* requested events */
public short revents; /* returned events */
};
enum FbIoCtl : uint
{
FBIOGET_VSCREENINFO = 0x4600,
@ -188,7 +199,7 @@ namespace Avalonia.LinuxFramebuffer
unsafe struct fd_set
{
public int count;
public fixed int fds [256];
public fixed byte fds [256];
}
enum AxisEventCode

292
src/Linux/Avalonia.LinuxFramebuffer/Output/Drm.cs

@ -0,0 +1,292 @@
using System;
using System.Runtime.InteropServices;
// ReSharper disable FieldCanBeMadeReadOnly.Global
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable FieldCanBeMadeReadOnly.Local
namespace Avalonia.LinuxFramebuffer.Output
{
public enum DrmModeConnection
{
DRM_MODE_CONNECTED = 1,
DRM_MODE_DISCONNECTED = 2,
DRM_MODE_UNKNOWNCONNECTION = 3
}
public enum DrmModeSubPixel{
DRM_MODE_SUBPIXEL_UNKNOWN = 1,
DRM_MODE_SUBPIXEL_HORIZONTAL_RGB = 2,
DRM_MODE_SUBPIXEL_HORIZONTAL_BGR = 3,
DRM_MODE_SUBPIXEL_VERTICAL_RGB = 4,
DRM_MODE_SUBPIXEL_VERTICAL_BGR = 5,
DRM_MODE_SUBPIXEL_NONE = 6
}
static unsafe class LibDrm
{
private const string libdrm = "libdrm.so.2";
private const string libgbm = "libgbm.so.1";
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public unsafe delegate void DrmEventVBlankHandlerDelegate(int fd,
uint sequence,
uint tv_sec,
uint tv_usec,
void* user_data);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public unsafe delegate void DrmEventPageFlipHandlerDelegate(int fd,
uint sequence,
uint tv_sec,
uint tv_usec,
void* user_data);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public unsafe delegate IntPtr DrmEventPageFlipHandler2Delegate(int fd,
uint sequence,
uint tv_sec,
uint tv_usec,
uint crtc_id,
void* user_data);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public unsafe delegate void DrmEventSequenceHandlerDelegate(int fd,
ulong sequence,
ulong ns,
ulong user_data);
[StructLayout(LayoutKind.Sequential)]
public struct DrmEventContext
{
public int version; //4
public IntPtr vblank_handler;
public IntPtr page_flip_handler;
public IntPtr page_flip_handler2;
public IntPtr sequence_handler;
}
[StructLayout(LayoutKind.Sequential)]
public struct drmModeRes {
public int count_fbs;
public uint *fbs;
public int count_crtcs;
public uint *crtcs;
public int count_connectors;
public uint *connectors;
public int count_encoders;
public uint *encoders;
uint min_width, max_width;
uint min_height, max_height;
}
[Flags]
public enum DrmModeType
{
DRM_MODE_TYPE_BUILTIN = (1 << 0),
DRM_MODE_TYPE_CLOCK_C = ((1 << 1) | DRM_MODE_TYPE_BUILTIN),
DRM_MODE_TYPE_CRTC_C = ((1 << 2) | DRM_MODE_TYPE_BUILTIN),
DRM_MODE_TYPE_PREFERRED = (1 << 3),
DRM_MODE_TYPE_DEFAULT = (1 << 4),
DRM_MODE_TYPE_USERDEF = (1 << 5),
DRM_MODE_TYPE_DRIVER = (1 << 6)
}
[StructLayout(LayoutKind.Sequential)]
public struct drmModeModeInfo
{
public uint clock;
public ushort hdisplay, hsync_start, hsync_end, htotal, hskew;
public ushort vdisplay, vsync_start, vsync_end, vtotal, vscan;
public uint vrefresh;
public uint flags;
public DrmModeType type;
public fixed byte name[32];
public PixelSize Resolution => new PixelSize(hdisplay, vdisplay);
}
[StructLayout(LayoutKind.Sequential)]
public struct drmModeConnector {
public uint connector_id;
public uint encoder_id; /**< Encoder currently connected to */
public uint connector_type;
public uint connector_type_id;
public DrmModeConnection connection;
public uint mmWidth, mmHeight; /**< HxW in millimeters */
public DrmModeSubPixel subpixel;
public int count_modes;
public drmModeModeInfo* modes;
public int count_props;
public uint *props; /**< List of property ids */
public ulong *prop_values; /**< List of property values */
public int count_encoders;
public uint *encoders; /**< List of encoder ids */
}
[StructLayout(LayoutKind.Sequential)]
public struct drmModeEncoder {
public uint encoder_id;
public uint encoder_type;
public uint crtc_id;
public uint possible_crtcs;
public uint possible_clones;
}
[StructLayout(LayoutKind.Sequential)]
public struct drmModeCrtc {
public uint crtc_id;
public uint buffer_id; /**< FB id to connect to 0 = disconnect */
public uint x, y; /**< Position on the framebuffer */
public uint width, height;
public int mode_valid;
public drmModeModeInfo mode;
public int gamma_size; /**< Number of gamma stops */
}
[DllImport(libdrm, SetLastError = true)]
public static extern drmModeRes* drmModeGetResources(int fd);
[DllImport(libdrm, SetLastError = true)]
public static extern void drmModeFreeResources(drmModeRes* res);
[DllImport(libdrm, SetLastError = true)]
public static extern drmModeConnector* drmModeGetConnector(int fd, uint connector);
[DllImport(libdrm, SetLastError = true)]
public static extern void drmModeFreeConnector(drmModeConnector* res);
[DllImport(libdrm, SetLastError = true)]
public static extern drmModeEncoder* drmModeGetEncoder(int fd, uint id);
[DllImport(libdrm, SetLastError = true)]
public static extern void drmModeFreeEncoder(drmModeEncoder* enc);
[DllImport(libdrm, SetLastError = true)]
public static extern drmModeCrtc* drmModeGetCrtc(int fd, uint id);
[DllImport(libdrm, SetLastError = true)]
public static extern void drmModeFreeCrtc(drmModeCrtc* enc);
[DllImport(libdrm, SetLastError = true)]
public static extern int drmModeAddFB(int fd, uint width, uint height, byte depth,
byte bpp, uint pitch, uint bo_handle,
out uint buf_id);
[DllImport(libdrm, SetLastError = true)]
public static extern int drmModeSetCrtc(int fd, uint crtcId, uint bufferId,
uint x, uint y, uint *connectors, int count,
drmModeModeInfo* mode);
[DllImport(libdrm, SetLastError = true)]
public static extern void drmModeRmFB(int fd, int id);
[Flags]
public enum DrmModePageFlip
{
Event = 1,
Async = 2,
Absolute = 4,
Relative = 8,
}
[DllImport(libdrm, SetLastError = true)]
public static extern void drmModePageFlip(int fd, uint crtc_id, uint fb_id,
DrmModePageFlip flags, void *user_data);
[DllImport(libdrm, SetLastError = true)]
public static extern void drmHandleEvent(int fd, DrmEventContext* context);
[DllImport(libgbm, SetLastError = true)]
public static extern IntPtr gbm_create_device(int fd);
[Flags]
public enum GbmBoFlags {
/**
* Buffer is going to be presented to the screen using an API such as KMS
*/
GBM_BO_USE_SCANOUT = (1 << 0),
/**
* Buffer is going to be used as cursor
*/
GBM_BO_USE_CURSOR = (1 << 1),
/**
* Deprecated
*/
GBM_BO_USE_CURSOR_64X64 = GBM_BO_USE_CURSOR,
/**
* Buffer is to be used for rendering - for example it is going to be used
* as the storage for a color buffer
*/
GBM_BO_USE_RENDERING = (1 << 2),
/**
* Buffer can be used for gbm_bo_write. This is guaranteed to work
* with GBM_BO_USE_CURSOR, but may not work for other combinations.
*/
GBM_BO_USE_WRITE = (1 << 3),
/**
* Buffer is linear, i.e. not tiled.
*/
GBM_BO_USE_LINEAR = (1 << 4),
};
[DllImport(libgbm, SetLastError = true)]
public static extern IntPtr gbm_surface_create(IntPtr device, int width, int height, uint format, GbmBoFlags flags);
[DllImport(libgbm, SetLastError = true)]
public static extern IntPtr gbm_surface_lock_front_buffer(IntPtr surface);
[DllImport(libgbm, SetLastError = true)]
public static extern int gbm_surface_release_buffer(IntPtr surface, IntPtr bo);
[DllImport(libgbm, SetLastError = true)]
public static extern IntPtr gbm_bo_get_user_data(IntPtr surface);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void GbmBoUserDataDestroyCallbackDelegate(IntPtr bo, IntPtr data);
[DllImport(libgbm, SetLastError = true)]
public static extern IntPtr gbm_bo_set_user_data(IntPtr bo, IntPtr userData,
GbmBoUserDataDestroyCallbackDelegate onFree);
[DllImport(libgbm, SetLastError = true)]
public static extern uint gbm_bo_get_width(IntPtr bo);
[DllImport(libgbm, SetLastError = true)]
public static extern uint gbm_bo_get_height(IntPtr bo);
[DllImport(libgbm, SetLastError = true)]
public static extern uint gbm_bo_get_stride(IntPtr bo);
[StructLayout(LayoutKind.Explicit)]
public struct GbmBoHandle
{
[FieldOffset(0)]
public void *ptr;
[FieldOffset(0)]
public int s32;
[FieldOffset(0)]
public uint u32;
[FieldOffset(0)]
public long s64;
[FieldOffset(0)]
public ulong u64;
}
[DllImport(libgbm, SetLastError = true)]
public static extern ulong gbm_bo_get_handle(IntPtr bo);
public static class GbmColorFormats
{
public static uint FourCC(char a, char b, char c, char d) =>
(uint)a | ((uint)b) << 8 | ((uint)c) << 16 | ((uint)d) << 24;
public static uint GBM_FORMAT_XRGB8888 { get; } = FourCC('X', 'R', '2', '4');
}
}
}

158
src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs

@ -0,0 +1,158 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.InteropServices;
using static Avalonia.LinuxFramebuffer.NativeUnsafeMethods;
using static Avalonia.LinuxFramebuffer.Output.LibDrm;
namespace Avalonia.LinuxFramebuffer.Output
{
public unsafe class DrmConnector
{
private static string[] KnownConnectorTypes =
{
"None", "VGA", "DVI-I", "DVI-D", "DVI-A", "Composite", "S-Video", "LVDS", "Component", "DIN",
"DisplayPort", "HDMI-A", "HDMI-B", "TV", "eDP", "Virtual", "DSI"
};
public DrmModeConnection Connection { get; }
public uint Id { get; }
public string Name { get; }
public Size SizeMm { get; }
public DrmModeSubPixel SubPixel { get; }
internal uint EncoderId { get; }
internal List<uint> EncoderIds { get; } = new List<uint>();
public List<DrmModeInfo> Modes { get; } = new List<DrmModeInfo>();
internal DrmConnector(drmModeConnector* conn)
{
Connection = conn->connection;
Id = conn->connector_id;
SizeMm = new Size(conn->mmWidth, conn->mmHeight);
SubPixel = conn->subpixel;
for (var c = 0; c < conn->count_encoders;c++)
EncoderIds.Add(conn->encoders[c]);
EncoderId = conn->encoder_id;
for(var c=0; c<conn->count_modes; c++)
Modes.Add(new DrmModeInfo(ref conn->modes[c]));
if (conn->connector_type > KnownConnectorTypes.Length - 1)
Name = $"Unknown({conn->connector_type})-{conn->connector_type_id}";
else
Name = KnownConnectorTypes[conn->connector_type] + "-" + conn->connector_type_id;
}
}
public unsafe class DrmModeInfo
{
internal drmModeModeInfo Mode;
internal DrmModeInfo(ref drmModeModeInfo info)
{
Mode = info;
fixed (void* pName = info.name)
Name = Marshal.PtrToStringAnsi(new IntPtr(pName));
}
public PixelSize Resolution => new PixelSize(Mode.hdisplay, Mode.vdisplay);
public bool IsPreferred => Mode.type.HasFlag(DrmModeType.DRM_MODE_TYPE_PREFERRED);
public string Name { get; }
}
unsafe class DrmEncoder
{
public drmModeEncoder Encoder { get; }
public List<drmModeCrtc> PossibleCrtcs { get; } = new List<drmModeCrtc>();
public DrmEncoder(drmModeEncoder encoder, drmModeCrtc[] crtcs)
{
Encoder = encoder;
for (var c = 0; c < crtcs.Length; c++)
{
var bit = 1 << c;
if ((encoder.possible_crtcs & bit) != 0)
PossibleCrtcs.Add(crtcs[c]);
}
}
}
public unsafe class DrmResources
{
public List<DrmConnector> Connectors { get; }= new List<DrmConnector>();
internal Dictionary<uint, DrmEncoder> Encoders { get; } = new Dictionary<uint, DrmEncoder>();
public DrmResources(int fd)
{
var res = drmModeGetResources(fd);
if (res == null)
throw new Win32Exception("drmModeGetResources failed");
var crtcs = new drmModeCrtc[res->count_crtcs];
for (var c = 0; c < res->count_crtcs; c++)
{
var crtc = drmModeGetCrtc(fd, res->crtcs[c]);
crtcs[c] = *crtc;
drmModeFreeCrtc(crtc);
}
for (var c = 0; c < res->count_encoders; c++)
{
var enc = drmModeGetEncoder(fd, res->encoders[c]);
Encoders[res->encoders[c]] = new DrmEncoder(*enc, crtcs);
drmModeFreeEncoder(enc);
}
for (var c = 0; c < res->count_connectors; c++)
{
var conn = drmModeGetConnector(fd, res->connectors[c]);
Connectors.Add(new DrmConnector(conn));
drmModeFreeConnector(conn);
}
}
public void Dump()
{
void Print(int off, string s)
{
for (var c = 0; c < off; c++)
Console.Write(" ");
Console.WriteLine(s);
}
Print(0, "Connectors");
foreach (var conn in Connectors)
{
Print(1, $"{conn.Name}:");
Print(2, $"Id: {conn.Id}");
Print(2, $"Size: {conn.SizeMm} mm");
Print(2, $"Encoder id: {conn.EncoderId}");
Print(2, "Modes");
foreach (var m in conn.Modes)
Print(3, $"{m.Name} {(m.IsPreferred ? "PREFERRED" : "")}");
}
}
}
public unsafe class DrmCard : IDisposable
{
public int Fd { get; private set; }
public DrmCard(string path = null)
{
path = path ?? "/dev/dri/card0";
Fd = open(path, 2, 0);
if (Fd == -1)
throw new Win32Exception("Couldn't open " + path);
}
public DrmResources GetResources() => new DrmResources(Fd);
public void Dispose()
{
close(Fd);
Fd = -1;
}
}
}

250
src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs

@ -0,0 +1,250 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.InteropServices;
using Avalonia.OpenGL;
using Avalonia.Platform.Interop;
using static Avalonia.LinuxFramebuffer.NativeUnsafeMethods;
using static Avalonia.LinuxFramebuffer.Output.LibDrm;
namespace Avalonia.LinuxFramebuffer.Output
{
public unsafe class DrmOutput : IOutputBackend, IGlPlatformSurface, IWindowingPlatformGlFeature
{
private DrmCard _card;
private readonly EglGlPlatformSurface _eglPlatformSurface;
public PixelSize PixelSize => _mode.Resolution;
public DrmOutput(string path = null)
{
var card = new DrmCard(path);
var resources = card.GetResources();
var connector =
resources.Connectors.FirstOrDefault(x => x.Connection == DrmModeConnection.DRM_MODE_CONNECTED);
if(connector == null)
throw new InvalidOperationException("Unable to find connected DRM connector");
var mode = connector.Modes.OrderByDescending(x => x.IsPreferred)
.ThenByDescending(x => x.Resolution.Width * x.Resolution.Height)
//.OrderByDescending(x => x.Resolution.Width * x.Resolution.Height)
.FirstOrDefault();
if(mode == null)
throw new InvalidOperationException("Unable to find a usable DRM mode");
Init(card, resources, connector, mode);
}
public DrmOutput(DrmCard card, DrmResources resources, DrmConnector connector, DrmModeInfo modeInfo)
{
Init(card, resources, connector, modeInfo);
}
[DllImport("libEGL.so.1")]
static extern IntPtr eglGetProcAddress(Utf8Buffer proc);
private GbmBoUserDataDestroyCallbackDelegate FbDestroyDelegate;
private drmModeModeInfo _mode;
private EglDisplay _eglDisplay;
private EglSurface _eglSurface;
private EglContext _immediateContext;
private EglContext _deferredContext;
private IntPtr _currentBo;
private IntPtr _gbmTargetSurface;
private uint _crtcId;
void FbDestroyCallback(IntPtr bo, IntPtr userData)
{
drmModeRmFB(_card.Fd, userData.ToInt32());
}
uint GetFbIdForBo(IntPtr bo)
{
if (bo == IntPtr.Zero)
throw new ArgumentException("bo is 0");
var data = gbm_bo_get_user_data(bo);
if (data != IntPtr.Zero)
return (uint)data.ToInt32();
var w = gbm_bo_get_width(bo);
var h = gbm_bo_get_height(bo);
var stride = gbm_bo_get_stride(bo);
var handle = gbm_bo_get_handle(bo);
var ret = drmModeAddFB(_card.Fd, w, h, 24, 32, stride, (uint)handle, out var fbHandle);
if (ret != 0)
throw new Win32Exception(ret, "drmModeAddFb failed");
gbm_bo_set_user_data(bo, new IntPtr((int)fbHandle), FbDestroyDelegate);
return fbHandle;
}
void Init(DrmCard card, DrmResources resources, DrmConnector connector, DrmModeInfo modeInfo)
{
FbDestroyDelegate = FbDestroyCallback;
_card = card;
uint GetCrtc()
{
if (resources.Encoders.TryGetValue(connector.EncoderId, out var encoder))
{
// Not sure why that should work
return encoder.Encoder.crtc_id;
}
else
{
foreach (var encId in connector.EncoderIds)
{
if (resources.Encoders.TryGetValue(encId, out encoder)
&& encoder.PossibleCrtcs.Count>0)
return encoder.PossibleCrtcs.First().crtc_id;
}
throw new InvalidOperationException("Unable to find CRTC matching the desired mode");
}
}
_crtcId = GetCrtc();
var device = gbm_create_device(card.Fd);
_gbmTargetSurface = gbm_surface_create(device, modeInfo.Resolution.Width, modeInfo.Resolution.Height,
GbmColorFormats.GBM_FORMAT_XRGB8888, GbmBoFlags.GBM_BO_USE_SCANOUT | GbmBoFlags.GBM_BO_USE_RENDERING);
if(_gbmTargetSurface == null)
throw new InvalidOperationException("Unable to create GBM surface");
_eglDisplay = new EglDisplay(new EglInterface(eglGetProcAddress), 0x31D7, device, null);
_eglSurface = _eglDisplay.CreateWindowSurface(_gbmTargetSurface);
EglContext CreateContext(EglContext share)
{
var offSurf = gbm_surface_create(device, 1, 1, GbmColorFormats.GBM_FORMAT_XRGB8888,
GbmBoFlags.GBM_BO_USE_RENDERING);
if (offSurf == null)
throw new InvalidOperationException("Unable to create 1x1 sized GBM surface");
return _eglDisplay.CreateContext(share, _eglDisplay.CreateWindowSurface(offSurf));
}
_immediateContext = CreateContext(null);
_deferredContext = CreateContext(_immediateContext);
_immediateContext.MakeCurrent(_eglSurface);
_eglDisplay.GlInterface.ClearColor(0, 0, 0, 0);
_eglDisplay.GlInterface.Clear(GlConsts.GL_COLOR_BUFFER_BIT | GlConsts.GL_STENCIL_BUFFER_BIT);
_eglSurface.SwapBuffers();
var bo = gbm_surface_lock_front_buffer(_gbmTargetSurface);
var fbId = GetFbIdForBo(bo);
var connectorId = connector.Id;
var mode = modeInfo.Mode;
var res = drmModeSetCrtc(_card.Fd, _crtcId, fbId, 0, 0, &connectorId, 1, &mode);
if (res != 0)
throw new Win32Exception(res, "drmModeSetCrtc failed");
_mode = mode;
_currentBo = bo;
// Go trough two cycles of buffer swapping (there are render artifacts otherwise)
for(var c=0;c<2;c++)
using (CreateGlRenderTarget().BeginDraw())
{
_eglDisplay.GlInterface.ClearColor(0, 0, 0, 0);
_eglDisplay.GlInterface.Clear(GlConsts.GL_COLOR_BUFFER_BIT | GlConsts.GL_STENCIL_BUFFER_BIT);
}
}
public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget()
{
return new RenderTarget(this);
}
class RenderTarget : IGlPlatformSurfaceRenderTarget
{
private readonly DrmOutput _parent;
public RenderTarget(DrmOutput parent)
{
_parent = parent;
}
public void Dispose()
{
// We are wrapping GBM buffer chain associated with CRTC, and don't free it on a whim
}
class RenderSession : IGlPlatformSurfaceRenderingSession
{
private readonly DrmOutput _parent;
public RenderSession(DrmOutput parent)
{
_parent = parent;
}
public void Dispose()
{
_parent._eglDisplay.GlInterface.Flush();
_parent._eglSurface.SwapBuffers();
var nextBo = gbm_surface_lock_front_buffer(_parent._gbmTargetSurface);
if (nextBo == IntPtr.Zero)
{
// Not sure what else can be done
Console.WriteLine("gbm_surface_lock_front_buffer failed");
}
else
{
var fb = _parent.GetFbIdForBo(nextBo);
bool waitingForFlip = true;
drmModePageFlip(_parent._card.Fd, _parent._crtcId, fb, DrmModePageFlip.Event, null);
DrmEventPageFlipHandlerDelegate flipCb =
(int fd, uint sequence, uint tv_sec, uint tv_usec, void* user_data) =>
{
waitingForFlip = false;
};
var cbHandle = GCHandle.Alloc(flipCb);
var ctx = new DrmEventContext
{
version = 4, page_flip_handler = Marshal.GetFunctionPointerForDelegate(flipCb)
};
while (waitingForFlip)
{
var pfd = new pollfd {events = 1, fd = _parent._card.Fd};
poll(&pfd, new IntPtr(1), -1);
drmHandleEvent(_parent._card.Fd, &ctx);
}
cbHandle.Free();
gbm_surface_release_buffer(_parent._gbmTargetSurface, _parent._currentBo);
_parent._currentBo = nextBo;
}
_parent._eglDisplay.ClearContext();
}
public IGlDisplay Display => _parent._eglDisplay;
public PixelSize Size => _parent._mode.Resolution;
public double Scaling => 1;
}
public IGlPlatformSurfaceRenderingSession BeginDraw()
{
_parent._deferredContext.MakeCurrent(_parent._eglSurface);
return new RenderSession(_parent);
}
}
IGlContext IWindowingPlatformGlFeature.ImmediateContext => _immediateContext;
}
}

11
src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebuffer.cs → src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs

@ -2,11 +2,12 @@
using System.Runtime.InteropServices;
using System.Text;
using Avalonia.Controls.Platform.Surfaces;
using Avalonia.LinuxFramebuffer.Output;
using Avalonia.Platform;
namespace Avalonia.LinuxFramebuffer
{
public sealed unsafe class LinuxFramebuffer : IFramebufferPlatformSurface, IDisposable
public sealed unsafe class FbdevOutput : IFramebufferPlatformSurface, IDisposable, IOutputBackend
{
private readonly Vector _dpi;
private int _fd;
@ -15,7 +16,7 @@ namespace Avalonia.LinuxFramebuffer
private IntPtr _mappedLength;
private IntPtr _mappedAddress;
public LinuxFramebuffer(string fileName = null, Vector? dpi = null)
public FbdevOutput(string fileName = null, Vector? dpi = null)
{
_dpi = dpi ?? new Vector(96, 96);
fileName = fileName ?? Environment.GetEnvironmentVariable("FRAMEBUFFER") ?? "/dev/fb0";
@ -85,14 +86,14 @@ namespace Avalonia.LinuxFramebuffer
public string Id { get; private set; }
public Size PixelSize
public PixelSize PixelSize
{
get
{
fb_var_screeninfo nfo;
if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOGET_VSCREENINFO, &nfo))
throw new Exception("FBIOGET_VSCREENINFO error: " + Marshal.GetLastWin32Error());
return new Size(nfo.xres, nfo.yres);
return new PixelSize((int)nfo.xres, (int)nfo.yres);
}
}
@ -123,7 +124,7 @@ namespace Avalonia.LinuxFramebuffer
GC.SuppressFinalize(this);
}
~LinuxFramebuffer()
~FbdevOutput()
{
ReleaseUnmanagedResources();
}

7
src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs

@ -0,0 +1,7 @@
namespace Avalonia.LinuxFramebuffer.Output
{
public interface IOutputBackend
{
PixelSize PixelSize { get; }
}
}

2
src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj

@ -12,7 +12,6 @@
<Compile Include="AvaloniaXamlLoader.cs" />
<Compile Include="Converters\AvaloniaUriTypeConverter.cs" />
<Compile Include="Converters\FontFamilyTypeConverter.cs" />
<Compile Include="Converters\MemberSelectorTypeConverter.cs" />
<Compile Include="Converters\TimeSpanTypeConverter.cs" />
<Compile Include="Extensions.cs" />
<Compile Include="MarkupExtension.cs" />
@ -33,7 +32,6 @@
<Compile Include="Templates\DataTemplate.cs" />
<Compile Include="Templates\FocusAdornerTemplate.cs" />
<Compile Include="Templates\ItemsPanelTemplate.cs" />
<Compile Include="Templates\MemberSelector.cs" />
<Compile Include="Templates\Template.cs" />
<Compile Include="Templates\TemplateContent.cs" />
<Compile Include="Templates\TreeDataTemplate.cs" />

24
src/Markup/Avalonia.Markup.Xaml/Converters/MemberSelectorTypeConverter.cs

@ -1,24 +0,0 @@
// 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.Globalization;
using Avalonia.Markup.Xaml.Templates;
namespace Avalonia.Markup.Xaml.Converters
{
using System.ComponentModel;
public class MemberSelectorTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
return MemberSelector.Parse((string)value);
}
}
}

48
src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs

@ -1,48 +0,0 @@
// 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.Controls.Templates;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.Markup.Parsers;
using System;
using System.Reactive.Linq;
namespace Avalonia.Markup.Xaml.Templates
{
public class MemberSelector : IMemberSelector
{
private string _memberName;
public string MemberName
{
get { return _memberName; }
set
{
if (_memberName != value)
{
_memberName = value;
}
}
}
public static MemberSelector Parse(string s)
{
return new MemberSelector { MemberName = s };
}
public object Select(object o)
{
if (string.IsNullOrEmpty(MemberName))
{
return o;
}
var expression = ExpressionObserverBuilder.Build(o, MemberName);
object result = AvaloniaProperty.UnsetValue;
expression.Subscribe(x => result = x);
return (result == AvaloniaProperty.UnsetValue || result is BindingNotification) ? null : result;
}
}
}

2
src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs

@ -103,8 +103,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
var ilist = typeSystem.GetType("System.Collections.Generic.IList`1");
AddType(ilist.MakeGenericType(typeSystem.GetType("Avalonia.Point")),
typeSystem.GetType("Avalonia.Markup.Xaml.Converters.PointsListTypeConverter"));
Add("Avalonia.Controls.Templates.IMemberSelector",
"Avalonia.Markup.Xaml.Converters.MemberSelectorTypeConverter");
Add("Avalonia.Controls.WindowIcon","Avalonia.Markup.Xaml.Converters.IconTypeConverter");
Add("System.Globalization.CultureInfo", "System.ComponentModel.CultureInfoConverter");
Add("System.Uri", "Avalonia.Markup.Xaml.Converters.AvaloniaUriTypeConverter");

78
tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Reactive.Subjects;
using Avalonia.Data;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Base.UnitTests
@ -34,10 +35,10 @@ namespace Avalonia.Base.UnitTests
{
var target = new Class1();
target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(6));
target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(new Exception(), BindingErrorType.Error));
target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(new Exception(), BindingErrorType.DataValidationError));
target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(7));
target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(6));
target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(new Exception(), BindingErrorType.Error));
target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(new Exception(), BindingErrorType.DataValidationError));
target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(7));
Assert.Equal(
new[]
@ -73,7 +74,7 @@ namespace Avalonia.Base.UnitTests
var source = new Subject<object>();
var target = new Class1
{
[!Class1.ValidatedDirectProperty] = source.ToBinding(),
[!Class1.ValidatedDirectIntProperty] = source.ToBinding(),
};
source.OnNext(new BindingNotification(6));
@ -92,6 +93,30 @@ namespace Avalonia.Base.UnitTests
target.Notifications.AsEnumerable());
}
[Fact]
public void Bound_Validated_Direct_String_Property_Can_Be_Set_To_Null()
{
var source = new ViewModel
{
StringValue = "foo",
};
var target = new Class1
{
[!Class1.ValidatedDirectStringProperty] = new Binding
{
Path = nameof(ViewModel.StringValue),
Source = source,
},
};
Assert.Equal("foo", target.ValidatedDirectString);
source.StringValue = null;
Assert.Null(target.ValidatedDirectString);
}
private class Class1 : AvaloniaObject
{
public static readonly StyledProperty<int> NonValidatedProperty =
@ -104,15 +129,23 @@ namespace Avalonia.Base.UnitTests
o => o.NonValidatedDirect,
(o, v) => o.NonValidatedDirect = v);
public static readonly DirectProperty<Class1, int> ValidatedDirectProperty =
public static readonly DirectProperty<Class1, int> ValidatedDirectIntProperty =
AvaloniaProperty.RegisterDirect<Class1, int>(
nameof(ValidatedDirect),
o => o.ValidatedDirect,
(o, v) => o.ValidatedDirect = v,
nameof(ValidatedDirectInt),
o => o.ValidatedDirectInt,
(o, v) => o.ValidatedDirectInt = v,
enableDataValidation: true);
public static readonly DirectProperty<Class1, string> ValidatedDirectStringProperty =
AvaloniaProperty.RegisterDirect<Class1, string>(
nameof(ValidatedDirectString),
o => o.ValidatedDirectString,
(o, v) => o.ValidatedDirectString = v,
enableDataValidation: true);
private int _nonValidatedDirect;
private int _direct;
private int _directInt;
private string _directString;
public int NonValidated
{
@ -122,14 +155,20 @@ namespace Avalonia.Base.UnitTests
public int NonValidatedDirect
{
get { return _direct; }
get { return _directInt; }
set { SetAndRaise(NonValidatedDirectProperty, ref _nonValidatedDirect, value); }
}
public int ValidatedDirect
public int ValidatedDirectInt
{
get { return _directInt; }
set { SetAndRaise(ValidatedDirectIntProperty, ref _directInt, value); }
}
public string ValidatedDirectString
{
get { return _direct; }
set { SetAndRaise(ValidatedDirectProperty, ref _direct, value); }
get { return _directString; }
set { SetAndRaise(ValidatedDirectStringProperty, ref _directString, value); }
}
public IList<BindingNotification> Notifications { get; } = new List<BindingNotification>();
@ -139,5 +178,16 @@ namespace Avalonia.Base.UnitTests
Notifications.Add(notification);
}
}
public class ViewModel : NotifyingBase
{
private string _stringValue;
public string StringValue
{
get { return _stringValue; }
set { _stringValue = value; RaisePropertyChanged(); }
}
}
}
}

2
tests/Avalonia.Base.UnitTests/PriorityValueTests.cs

@ -307,7 +307,7 @@ namespace Avalonia.Base.UnitTests
private static Mock<IPriorityValueOwner> GetMockOwner()
{
var owner = new Mock<IPriorityValueOwner>();
owner.SetupGet(o => o.Setter).Returns(new DeferredSetter<object>());
owner.Setup(o => o.GetNonDirectDeferredSetter(It.IsAny<AvaloniaProperty>())).Returns(new DeferredSetter<object>());
return owner;
}
}

2
tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj

@ -18,7 +18,7 @@
<ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.10.14" />
<PackageReference Include="BenchmarkDotNet" Version="0.11.5" />
</ItemGroup>
<ItemGroup>
<Folder Include="Properties\" />

65
tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs

@ -0,0 +1,65 @@
using BenchmarkDotNet.Attributes;
namespace Avalonia.Benchmarks.Base
{
[MemoryDiagnoser]
public class DirectPropertyBenchmark
{
[Benchmark(Baseline = true)]
public void SetAndRaiseOriginal()
{
var obj = new DirectClass();
for (var i = 0; i < 100; ++i)
{
obj.IntValue += 1;
}
}
[Benchmark]
public void SetAndRaiseSimple()
{
var obj = new DirectClass();
for (var i = 0; i < 100; ++i)
{
obj.IntValueSimple += 1;
}
}
class DirectClass : AvaloniaObject
{
private int _intValue;
public static readonly DirectProperty<DirectClass, int> IntValueProperty =
AvaloniaProperty.RegisterDirect<DirectClass, int>(nameof(IntValue),
o => o.IntValue,
(o, v) => o.IntValue = v);
public int IntValue
{
get => _intValue;
set => SetAndRaise(IntValueProperty, ref _intValue, value);
}
public int IntValueSimple
{
get => _intValue;
set
{
VerifyAccess();
if (_intValue == value)
{
return;
}
var old = _intValue;
_intValue = value;
RaisePropertyChanged(IntValueProperty, old, _intValue);
}
}
}
}
}

3
tests/Avalonia.Benchmarks/Base/Properties.cs

@ -1,5 +1,4 @@
using System;
using System.Reactive.Subjects;
using System.Reactive.Subjects;
using BenchmarkDotNet.Attributes;
namespace Avalonia.Benchmarks.Base

54
tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs

@ -16,6 +16,60 @@ namespace Avalonia.Controls.UnitTests
private Mock<IPopupImpl> popupImpl;
private MouseTestHelper _mouse = new MouseTestHelper();
[Fact]
public void Opening_Raises_Single_Opened_Event()
{
using (Application())
{
var sut = new ContextMenu();
var target = new Panel
{
ContextMenu = sut
};
new Window { Content = target };
int openedCount = 0;
sut.MenuOpened += (sender, args) =>
{
openedCount++;
};
sut.Open(null);
Assert.Equal(1, openedCount);
}
}
[Fact]
public void Closing_Raises_Single_Closed_Event()
{
using (Application())
{
var sut = new ContextMenu();
var target = new Panel
{
ContextMenu = sut
};
new Window { Content = target };
sut.Open(null);
int closedCount = 0;
sut.MenuClosed += (sender, args) =>
{
closedCount++;
};
sut.Close();
Assert.Equal(1, closedCount);
}
}
[Fact]
public void Clicking_On_Control_Toggles_ContextMenu()
{

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

Loading…
Cancel
Save