Browse Source

Merge branch 'master' into fixes/Visual_Warnings

pull/6505/head
Steven Kirk 4 years ago
committed by GitHub
parent
commit
5d272d8bd2
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      Avalonia.sln.DotSettings
  2. 1
      Documentation/build.md
  3. 2
      azure-pipelines.yml
  4. 10
      native/Avalonia.Native/src/OSX/window.mm
  5. 16
      packages/Avalonia/AvaloniaBuildTasks.targets
  6. 1
      samples/ControlCatalog/Pages/TextBoxPage.xaml
  7. 2
      src/Avalonia.Base/Data/Converters/FuncValueConverter.cs
  8. 2
      src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs
  9. 14
      src/Avalonia.Base/Utilities/TypeUtilities.cs
  10. 51
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  11. 49
      src/Avalonia.Controls.DataGrid/DataGridCell.cs
  12. 11
      src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs
  13. 36
      src/Avalonia.Controls.DataGrid/DataGridRow.cs
  14. 15
      src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs
  15. 17
      src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs
  16. 16
      src/Avalonia.Controls/AutoCompleteBox.cs
  17. 15
      src/Avalonia.Controls/Design.cs
  18. 10
      src/Avalonia.Controls/Flyouts/FlyoutBase.cs
  19. 433
      src/Avalonia.Controls/MaskedTextBox.cs
  20. 5
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  21. 2
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  22. 23
      src/Avalonia.Controls/Primitives/Popup.cs
  23. 73
      src/Avalonia.Controls/TextBox.cs
  24. 4
      src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs
  25. 4
      src/Avalonia.Dialogs/ManagedFileChooser.cs
  26. 2
      src/Avalonia.Dialogs/ManagedFileChooserSources.cs
  27. 2
      src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  28. 5
      src/Avalonia.Input/AccessKeyHandler.cs
  29. 1
      src/Avalonia.Themes.Default/Expander.xaml
  30. 4
      src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
  31. 2
      src/Avalonia.Themes.Fluent/Controls/NotificationCard.xaml
  32. 6
      src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml
  33. 4
      src/Avalonia.Visuals/ApiCompatBaseline.txt
  34. 170
      src/Avalonia.Visuals/Media/CombinedGeometry.cs
  35. 2
      src/Avalonia.Visuals/Media/FormattedText.cs
  36. 37
      src/Avalonia.Visuals/Media/GeometryCollection.cs
  37. 80
      src/Avalonia.Visuals/Media/GeometryGroup.cs
  38. 10
      src/Avalonia.Visuals/Media/PathMarkupParser.cs
  39. 52
      src/Avalonia.Visuals/Media/RotateTransform.cs
  40. 17
      src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs
  41. 33
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  42. 7
      src/Avalonia.Visuals/Visual.cs
  43. 8
      src/Avalonia.X11/X11Window.cs
  44. 9
      src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs
  45. 7
      src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs
  46. 14
      src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatformOptions.cs
  47. 46
      src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs
  48. 70
      src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevBackBuffer.cs
  49. 7
      src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs
  50. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github
  51. 23
      src/Markup/Avalonia.Markup.Xaml/Extensions.cs
  52. 18
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs
  53. 23
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ReflectionBindingExtension.cs
  54. 35
      src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs
  55. 47
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  56. 4
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  57. 36
      src/Skia/Avalonia.Skia/GeometryGroupImpl.cs
  58. 10
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  59. 2
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  60. 4
      src/Windows/Avalonia.Direct2D1/Media/AvaloniaTextRenderer.cs
  61. 36
      src/Windows/Avalonia.Direct2D1/Media/CombinedGeometryImpl.cs
  62. 32
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  63. 33
      src/Windows/Avalonia.Direct2D1/Media/GeometryGroupImpl.cs
  64. 7
      src/Windows/Avalonia.Direct2D1/Media/LinearGradientBrushImpl.cs
  65. 11
      src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs
  66. 25
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  67. 6
      src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs
  68. 6
      src/Windows/Avalonia.Win32/WindowImpl.cs
  69. 1
      tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj
  70. 73
      tests/Avalonia.Base.UnitTests/Logging/LoggingTests.cs
  71. 10
      tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
  72. 10
      tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs
  73. 990
      tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs
  74. 89
      tests/Avalonia.RenderTests/Media/CombinedGeometryTests.cs
  75. 3
      tests/Avalonia.RenderTests/Media/ConicGradientBrushTests.cs
  76. 95
      tests/Avalonia.RenderTests/Media/GeometryGroupTests.cs
  77. 12
      tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs
  78. 3
      tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs
  79. 10
      tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs
  80. 26
      tests/Avalonia.Visuals.UnitTests/Media/GeometryGroupTests.cs
  81. 23
      tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs
  82. 10
      tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs
  83. BIN
      tests/TestFiles/Direct2D1/Media/CombinedGeometry/Geometry1_Transform.expected.png
  84. BIN
      tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Exclude.expected.png
  85. BIN
      tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Intersect.expected.png
  86. BIN
      tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Union.expected.png
  87. BIN
      tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Xor.expected.png
  88. BIN
      tests/TestFiles/Direct2D1/Media/GeometryGroup/Child_Transform.expected.png
  89. BIN
      tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_Stroke_EvenOdd.expected.png
  90. BIN
      tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_Stroke_NonZero.expected.png
  91. BIN
      tests/TestFiles/Skia/Media/CombinedGeometry/Geometry1_Transform.expected.png
  92. BIN
      tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Exclude.expected.png
  93. BIN
      tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Intersect.expected.png
  94. BIN
      tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Union.expected.png
  95. BIN
      tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Xor.expected.png
  96. BIN
      tests/TestFiles/Skia/Media/GeometryGroup/Child_Transform.expected.png
  97. BIN
      tests/TestFiles/Skia/Media/GeometryGroup/FillRule_Stroke_EvenOdd.expected.png
  98. BIN
      tests/TestFiles/Skia/Media/GeometryGroup/FillRule_Stroke_NonZero.expected.png

3
Avalonia.sln.DotSettings

@ -1,5 +1,4 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_002Examl/@EntryIndexedValue">True</s:Boolean>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=3E53A01A_002DB331_002D47F3_002DB828_002D4A5717E77A24_002Fd_003Aglass/@EntryIndexedValue">ExplicitlyExcluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=6417B24E_002D49C2_002D4985_002D8DB2_002D3AB9D898EC91/@EntryIndexedValue">ExplicitlyExcluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=E3A1060B_002D50D0_002D44E8_002D88B6_002DF44EF2E5BD72_002Ff_003Ahtml_002Ehtm/@EntryIndexedValue">ExplicitlyExcluded</s:String>
@ -39,4 +38,4 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=TypesAndNamespaces/@EntryIndexedValue">&lt;Policy Inspect="False" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EFeature_002EServices_002EDaemon_002ESettings_002EMigration_002ESwaWarningsModeSettingsMigrate/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Avalonia/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Fcitx/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Fcitx/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

1
Documentation/build.md

@ -6,6 +6,7 @@ Avalonia requires at least Visual Studio 2019 and .NET Core SDK 3.1 to build on
```
git clone https://github.com/AvaloniaUI/Avalonia.git
cd Avalonia
git submodule update --init
```

2
azure-pipelines.yml

@ -1,7 +1,7 @@
jobs:
- job: Linux
pool:
vmImage: 'ubuntu-16.04'
vmImage: 'ubuntu-20.04'
steps:
- task: CmdLine@2
displayName: 'Install Nuke'

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

@ -641,6 +641,7 @@ private:
[Window setCanBecomeKeyAndMain];
[Window disableCursorRects];
[Window setTabbingMode:NSWindowTabbingModeDisallowed];
[Window setCollectionBehavior:NSWindowCollectionBehaviorFullScreenPrimary];
}
void HideOrShowTrafficLights ()
@ -1091,14 +1092,7 @@ private:
{
_fullScreenActive = true;
[Window setHasShadow:YES];
[Window setTitleVisibility:NSWindowTitleVisible];
[Window setTitlebarAppearsTransparent:NO];
[Window setTitle:_lastTitle];
Window.styleMask = Window.styleMask | NSWindowStyleMaskTitled | NSWindowStyleMaskResizable;
Window.styleMask = Window.styleMask & ~NSWindowStyleMaskFullSizeContentView;
[Window toggleFullScreen:nullptr];
}
@ -1672,6 +1666,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
switch(event.buttonNumber)
{
case 2:
case 3:
_isMiddlePressed = true;
[self mouseEvent:event withType:MiddleButtonDown];
@ -1704,6 +1699,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
{
switch(event.buttonNumber)
{
case 2:
case 3:
_isMiddlePressed = false;
[self mouseEvent:event withType:MiddleButtonUp];

16
packages/Avalonia/AvaloniaBuildTasks.targets

@ -42,12 +42,24 @@
</Target>
<PropertyGroup>
<BuildAvaloniaResourcesDependsOn>$(BuildAvaloniaResourcesDependsOn);AddAvaloniaResources;ResolveReferences</BuildAvaloniaResourcesDependsOn>
<BuildAvaloniaResourcesDependsOn>$(BuildAvaloniaResourcesDependsOn);AddAvaloniaResources;ResolveReferences;_GenerateAvaloniaResourcesDependencyCache</BuildAvaloniaResourcesDependsOn>
</PropertyGroup>
<Target Name="_GenerateAvaloniaResourcesDependencyCache" BeforeTargets="GenerateAvaloniaResources">
<ItemGroup>
<CustomAdditionalGenerateAvaloniaResourcesInputs Include="$(IntermediateOutputPath)/Avalonia/Resources.Inputs.cache" />
</ItemGroup>
<Hash ItemsToHash="@(AvaloniaResource);@(AvaloniaXaml);$(MSBuildAllProjects)">
<Output TaskParameter="HashResult" PropertyName="AvaloniaResourcesDependencyHash" />
</Hash>
<WriteLinesToFile Overwrite="true" File="$(IntermediateOutputPath)/Avalonia/Resources.Inputs.cache" Lines="$(AvaloniaResourcesDependencyHash)" WriteOnlyWhenDifferent="True" />
</Target>
<Target Name="GenerateAvaloniaResources"
BeforeTargets="CoreCompile;CoreResGen"
Inputs="@(AvaloniaResource);@(AvaloniaXaml);$(MSBuildAllProjects)"
Inputs="@(AvaloniaResource);@(AvaloniaXaml);@(CustomAdditionalGenerateAvaloniaResourcesInputs);$(MSBuildAllProjects)"
Outputs="$(AvaloniaResourcesTemporaryFilePath)"
DependsOnTargets="$(BuildAvaloniaResourcesDependsOn)">
<ItemGroup>

1
samples/ControlCatalog/Pages/TextBoxPage.xaml

@ -18,6 +18,7 @@
Watermark="Floating Watermark"
UseFloatingWatermark="True"
Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit."/>
<MaskedTextBox Width="200" ResetOnSpace="False" Mask="(LLL) 999-0000"/>
<TextBox Width="200" Text="Validation Error">
<DataValidationErrors.Error>

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

@ -26,7 +26,7 @@ namespace Avalonia.Data.Converters
/// <inheritdoc/>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is TIn || (value == null && TypeUtilities.AcceptsNull(typeof(TIn))))
if (TypeUtilities.CanCast<TIn>(value))
{
return _convert((TIn)value);
}

2
src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs

@ -39,7 +39,7 @@ namespace Avalonia.Threading
if (Dispatcher.UIThread.CheckAccess())
d(state);
else
Dispatcher.UIThread.InvokeAsync(() => d(state), DispatcherPriority.Send).Wait();
Dispatcher.UIThread.InvokeAsync(() => d(state), DispatcherPriority.Send).GetAwaiter().GetResult();
}

14
src/Avalonia.Base/Utilities/TypeUtilities.cs

@ -3,6 +3,7 @@ using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace Avalonia.Utilities
{
@ -93,6 +94,17 @@ namespace Avalonia.Utilities
return !type.IsValueType || IsNullableType(type);
}
/// <summary>
/// Returns a value indicating whether null can be assigned to the specified type.
/// </summary>
/// <typeparam name="T">The type</typeparam>
/// <returns>True if the type accepts null values; otherwise false.</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool AcceptsNull<T>()
{
return default(T) is null;
}
/// <summary>
/// Returns a value indicating whether value can be casted to the specified type.
/// If value is null, checks if instances of that type can be null.
@ -102,7 +114,7 @@ namespace Avalonia.Utilities
/// <returns>True if the cast is possible, otherwise false.</returns>
public static bool CanCast<T>(object value)
{
return value is T || (value is null && AcceptsNull(typeof(T)));
return value is T || (value is null && AcceptsNull<T>());
}
/// <summary>

51
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -3039,6 +3039,12 @@ namespace Avalonia.Controls
}
}
//TODO: Ensure right button is checked for
internal bool UpdateStateOnMouseRightButtonDown(PointerPressedEventArgs pointerPressedEventArgs, int columnIndex, int slot, bool allowEdit)
{
KeyboardHelper.GetMetaKeyState(pointerPressedEventArgs.KeyModifiers, out bool ctrl, out bool shift);
return UpdateStateOnMouseRightButtonDown(pointerPressedEventArgs, columnIndex, slot, allowEdit, shift, ctrl);
}
//TODO: Ensure left button is checked for
internal bool UpdateStateOnMouseLeftButtonDown(PointerPressedEventArgs pointerPressedEventArgs, int columnIndex, int slot, bool allowEdit)
{
@ -4489,17 +4495,27 @@ namespace Avalonia.Controls
element = dataGridColumn.GenerateEditingElementInternal(dataGridCell, dataGridRow.DataContext);
if (element != null)
{
// Subscribe to the new element's events
element.Initialized += EditingElement_Initialized;
dataGridCell.Content = element;
if (element.IsInitialized)
{
PreparingCellForEditPrivate(element as Control);
}
else
{
// Subscribe to the new element's events
element.Initialized += EditingElement_Initialized;
}
}
}
else
{
// Generate Element and apply column style if available
element = dataGridColumn.GenerateElementInternal(dataGridCell, dataGridRow.DataContext);
dataGridCell.Content = element;
}
dataGridCell.Content = element;
}
private void PreparingCellForEditPrivate(Control editingElement)
@ -5711,6 +5727,35 @@ namespace Avalonia.Controls
VerticalScroll?.Invoke(sender, e);
}
//TODO: Ensure right button is checked for
private bool UpdateStateOnMouseRightButtonDown(PointerPressedEventArgs pointerPressedEventArgs, int columnIndex, int slot, bool allowEdit, bool shift, bool ctrl)
{
Debug.Assert(slot >= 0);
if (shift || ctrl)
{
return true;
}
if (IsSlotOutOfBounds(slot))
{
return true;
}
if (GetRowSelection(slot))
{
return true;
}
// Unselect everything except the row that was clicked on
try
{
UpdateSelectionAndCurrency(columnIndex, slot, DataGridSelectionAction.SelectCurrent, scrollIntoView: false);
}
finally
{
NoSelectionChangeCount--;
}
return true;
}
//TODO: Ensure left button is checked for
private bool UpdateStateOnMouseLeftButtonDown(PointerPressedEventArgs pointerPressedEventArgs, int columnIndex, int slot, bool allowEdit, bool shift, bool ctrl)
{

49
src/Avalonia.Controls.DataGrid/DataGridCell.cs

@ -161,29 +161,42 @@ namespace Avalonia.Controls
private void DataGridCell_PointerPressed(PointerPressedEventArgs e)
{
// OwningGrid is null for TopLeftHeaderCell and TopRightHeaderCell because they have no OwningRow
if (OwningGrid != null)
if (OwningGrid == null)
{
OwningGrid.OnCellPointerPressed(new DataGridCellPointerPressedEventArgs(this, OwningRow, OwningColumn, e));
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
return;
}
OwningGrid.OnCellPointerPressed(new DataGridCellPointerPressedEventArgs(this, OwningRow, OwningColumn, e));
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
if (!e.Handled)
//if (!e.Handled && OwningGrid.IsTabStop)
{
OwningGrid.Focus();
}
if (OwningRow != null)
{
if (!e.Handled)
//if (!e.Handled && OwningGrid.IsTabStop)
var handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled);
// Do not handle PointerPressed with touch,
// so we can start scroll gesture on the same event.
if (e.Pointer.Type != PointerType.Touch)
{
OwningGrid.Focus();
e.Handled = handled;
}
if (OwningRow != null)
{
var handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled);
// Do not handle PointerPressed with touch,
// so we can start scroll gesture on the same event.
if (e.Pointer.Type != PointerType.Touch)
{
e.Handled = handled;
}
OwningGrid.UpdatedStateOnMouseLeftButtonDown = true;
}
OwningGrid.UpdatedStateOnMouseLeftButtonDown = true;
}
}
else if (e.GetCurrentPoint(this).Properties.IsRightButtonPressed)
{
if (!e.Handled)
//if (!e.Handled && OwningGrid.IsTabStop)
{
OwningGrid.Focus();
}
if (OwningRow != null)
{
e.Handled = OwningGrid.UpdateStateOnMouseRightButtonDown(e, ColumnIndex, OwningRow.Slot, !e.Handled);
}
}
}

11
src/Avalonia.Controls.DataGrid/DataGridDataConnection.cs

@ -233,7 +233,7 @@ namespace Avalonia.Controls
else
{
editableCollectionView.EditItem(dataItem);
return editableCollectionView.IsEditingItem;
return editableCollectionView.IsEditingItem || editableCollectionView.IsAddingNew;
}
}
@ -314,7 +314,14 @@ namespace Avalonia.Controls
CommittingEdit = true;
try
{
editableCollectionView.CommitEdit();
if (editableCollectionView.IsAddingNew)
{
editableCollectionView.CommitNew();
}
else
{
editableCollectionView.CommitEdit();
}
}
finally
{

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

@ -378,13 +378,13 @@ namespace Avalonia.Controls
}
}
}
}
}
internal Panel RootElement
{
get;
private set;
}
}
internal int Slot
{
@ -638,7 +638,7 @@ namespace Avalonia.Controls
PseudoClasses.Set(":editing", IsEditing);
PseudoClasses.Set(":invalid", !IsValid);
ApplyHeaderStatus();
}
}
}
//TODO Animation
@ -896,7 +896,7 @@ namespace Avalonia.Controls
_detailsElement.ContentHeight = _detailsDesiredHeight;
}
}
}
}
// Makes sure the _detailsDesiredHeight is initialized. We need to measure it to know what
// height we want to animate to. Subsequently, we just update that height in response to SizeChanged
@ -919,7 +919,7 @@ namespace Avalonia.Controls
//TODO Cleanup
double? _previousDetailsHeight = null;
//TODO Animation
private void DetailsContent_HeightChanged(double newValue)
{
@ -1022,7 +1022,7 @@ namespace Avalonia.Controls
}
}
}
internal void ApplyDetailsTemplate(bool initializeDetailsPreferredHeight)
{
if (_detailsElement != null && AreDetailsVisible)
@ -1066,7 +1066,7 @@ namespace Avalonia.Controls
.Subscribe(DetailsContent_MarginChanged);
}
_detailsElement.Children.Add(_detailsContent);
}
}
@ -1090,6 +1090,28 @@ namespace Avalonia.Controls
}
}
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
if (change.Property == DataContextProperty)
{
var owner = OwningGrid;
if (owner != null && this.IsRecycled)
{
var columns = owner.ColumnsItemsInternal;
var nc = columns.Count;
for (int ci = 0; ci < nc; ci++)
{
if (columns[ci] is DataGridTemplateColumn column)
{
column.RefreshCellContent((Control)this.Cells[column.Index].Content, nameof(DataGridTemplateColumn.CellTemplate));
}
}
}
}
base.OnPropertyChanged(change);
}
}

15
src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs

@ -283,7 +283,11 @@ namespace Avalonia.Controls
//TODO TabStop
private void DataGridRowGroupHeader_PointerPressed(PointerPressedEventArgs e)
{
if (OwningGrid != null && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
if (OwningGrid == null)
{
return;
}
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
if (OwningGrid.IsDoubleClickRecordsClickOnCall(this) && !e.Handled)
{
@ -300,6 +304,15 @@ namespace Avalonia.Controls
e.Handled = OwningGrid.UpdateStateOnMouseLeftButtonDown(e, OwningGrid.CurrentColumnIndex, RowGroupInfo.Slot, allowEdit: false);
}
}
else if (e.GetCurrentPoint(this).Properties.IsRightButtonPressed)
{
if (!e.Handled)
{
OwningGrid.Focus();
}
e.Handled = OwningGrid.UpdateStateOnMouseRightButtonDown(e, OwningGrid.CurrentColumnIndex, RowGroupInfo.Slot, allowEdit: false);
}
}
private void EnsureChildClip(Visual child, double frozenLeftEdge)

17
src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs

@ -179,12 +179,12 @@ namespace Avalonia.Controls.Primitives
//TODO TabStop
private void DataGridRowHeader_PointerPressed(object sender, PointerPressedEventArgs e)
{
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
if (OwningGrid == null)
{
return;
}
if (OwningGrid != null)
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
if (!e.Handled)
//if (!e.Handled && OwningGrid.IsTabStop)
@ -199,6 +199,19 @@ namespace Avalonia.Controls.Primitives
OwningGrid.UpdatedStateOnMouseLeftButtonDown = true;
}
}
else if (e.GetCurrentPoint(this).Properties.IsRightButtonPressed)
{
if (!e.Handled)
{
OwningGrid.Focus();
}
if (OwningRow != null)
{
Debug.Assert(sender is DataGridRowHeader);
Debug.Assert(sender == this);
e.Handled = OwningGrid.UpdateStateOnMouseRightButtonDown(e, -1, Slot, false);
}
}
}
}

16
src/Avalonia.Controls/AutoCompleteBox.cs

@ -2094,7 +2094,21 @@ namespace Avalonia.Controls
bool inResults = !(stringFiltering || objectFiltering);
if (!inResults)
{
inResults = stringFiltering ? TextFilter(text, FormatValue(item)) : ItemFilter(text, item);
if (stringFiltering)
{
inResults = TextFilter(text, FormatValue(item));
}
else
{
if (ItemFilter is null)
{
throw new Exception("ItemFilter property can not be null when FilterMode has value AutoCompleteFilterMode.Custom");
}
else
{
inResults = ItemFilter(text, item);
}
}
}
if (view_count > view_index && inResults && _view[view_index] == item)

15
src/Avalonia.Controls/Design.cs

@ -60,6 +60,19 @@ namespace Avalonia.Controls
return target.GetValue(PreviewWithProperty);
}
public static readonly AttachedProperty<IStyle> DesignStyleProperty = AvaloniaProperty
.RegisterAttached<Control, IStyle>("DesignStyle", typeof(Design));
public static void SetDesignStyle(Control control, IStyle value)
{
control.SetValue(DesignStyleProperty, value);
}
public static IStyle GetDesignStyle(Control control)
{
return control.GetValue(DesignStyleProperty);
}
public static void ApplyDesignModeProperties(Control target, Control source)
{
if (source.IsSet(WidthProperty))
@ -68,6 +81,8 @@ namespace Avalonia.Controls
target.Height = source.GetValue(HeightProperty);
if (source.IsSet(DataContextProperty))
target.DataContext = source.GetValue(DataContextProperty);
if (source.IsSet(DesignStyleProperty))
target.Styles.Add(source.GetValue(DesignStyleProperty));
}
}
}

10
src/Avalonia.Controls/Flyouts/FlyoutBase.cs

@ -215,11 +215,6 @@ namespace Avalonia.Controls.Primitives
}
}
if (CancelOpening())
{
return false;
}
if (Popup.Parent != null && Popup.Parent != placementTarget)
{
((ISetLogicalParent)Popup).SetParent(null);
@ -236,6 +231,11 @@ namespace Avalonia.Controls.Primitives
Popup.Child = CreatePresenter();
}
if (CancelOpening())
{
return false;
}
PositionPopup(showAtPointer);
IsOpen = Popup.IsOpen = true;
OnOpened();

433
src/Avalonia.Controls/MaskedTextBox.cs

@ -0,0 +1,433 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Styling;
#nullable enable
namespace Avalonia.Controls
{
public class MaskedTextBox : TextBox, IStyleable
{
public static readonly StyledProperty<bool> AsciiOnlyProperty =
AvaloniaProperty.Register<MaskedTextBox, bool>(nameof(AsciiOnly));
public static readonly DirectProperty<MaskedTextBox, CultureInfo?> CultureProperty =
AvaloniaProperty.RegisterDirect<MaskedTextBox, CultureInfo?>(nameof(Culture), o => o.Culture,
(o, v) => o.Culture = v, CultureInfo.CurrentCulture);
public static readonly StyledProperty<bool> HidePromptOnLeaveProperty =
AvaloniaProperty.Register<MaskedTextBox, bool>(nameof(HidePromptOnLeave));
public static readonly DirectProperty<MaskedTextBox, bool?> MaskCompletedProperty =
AvaloniaProperty.RegisterDirect<MaskedTextBox, bool?>(nameof(MaskCompleted), o => o.MaskCompleted);
public static readonly DirectProperty<MaskedTextBox, bool?> MaskFullProperty =
AvaloniaProperty.RegisterDirect<MaskedTextBox, bool?>(nameof(MaskFull), o => o.MaskFull);
public static readonly StyledProperty<string?> MaskProperty =
AvaloniaProperty.Register<MaskedTextBox, string?>(nameof(Mask), string.Empty);
public static new readonly StyledProperty<char> PasswordCharProperty =
AvaloniaProperty.Register<TextBox, char>(nameof(PasswordChar), '\0');
public static readonly StyledProperty<char> PromptCharProperty =
AvaloniaProperty.Register<MaskedTextBox, char>(nameof(PromptChar), '_');
public static readonly DirectProperty<MaskedTextBox, bool> ResetOnPromptProperty =
AvaloniaProperty.RegisterDirect<MaskedTextBox, bool>(nameof(ResetOnPrompt), o => o.ResetOnPrompt, (o, v) => o.ResetOnPrompt = v);
public static readonly DirectProperty<MaskedTextBox, bool> ResetOnSpaceProperty =
AvaloniaProperty.RegisterDirect<MaskedTextBox, bool>(nameof(ResetOnSpace), o => o.ResetOnSpace, (o, v) => o.ResetOnSpace = v);
private CultureInfo? _culture;
private bool _resetOnPrompt = true;
private bool _ignoreTextChanges;
private bool _resetOnSpace = true;
public MaskedTextBox() { }
/// <summary>
/// Constructs the MaskedTextBox with the specified MaskedTextProvider object.
/// </summary>
public MaskedTextBox(MaskedTextProvider maskedTextProvider)
{
if (maskedTextProvider == null)
{
throw new ArgumentNullException(nameof(maskedTextProvider));
}
AsciiOnly = maskedTextProvider.AsciiOnly;
Culture = maskedTextProvider.Culture;
Mask = maskedTextProvider.Mask;
PasswordChar = maskedTextProvider.PasswordChar;
PromptChar = maskedTextProvider.PromptChar;
}
/// <summary>
/// Gets or sets a value indicating if the masked text box is restricted to accept only ASCII characters.
/// Default value is false.
/// </summary>
public bool AsciiOnly
{
get => GetValue(AsciiOnlyProperty);
set => SetValue(AsciiOnlyProperty, value);
}
/// <summary>
/// Gets or sets the culture information associated with the masked text box.
/// </summary>
public CultureInfo? Culture
{
get => _culture;
set => SetAndRaise(CultureProperty, ref _culture, value);
}
/// <summary>
/// Gets or sets a value indicating if the prompt character is hidden when the masked text box loses focus.
/// </summary>
public bool HidePromptOnLeave
{
get => GetValue(HidePromptOnLeaveProperty);
set => SetValue(HidePromptOnLeaveProperty, value);
}
/// <summary>
/// Gets or sets the mask to apply to the TextBox.
/// </summary>
public string? Mask
{
get => GetValue(MaskProperty);
set => SetValue(MaskProperty, value);
}
/// <summary>
/// Specifies whether the test string required input positions, as specified by the mask, have
/// all been assigned.
/// </summary>
public bool? MaskCompleted
{
get => MaskProvider?.MaskCompleted;
}
/// <summary>
/// Specifies whether all inputs (required and optional) have been provided into the mask successfully.
/// </summary>
public bool? MaskFull
{
get => MaskProvider?.MaskFull;
}
/// <summary>
/// Gets the MaskTextProvider for the specified Mask.
/// </summary>
public MaskedTextProvider? MaskProvider { get; private set; }
/// <summary>
/// Gets or sets the character to be displayed in substitute for user input.
/// </summary>
public new char PasswordChar
{
get => GetValue(PasswordCharProperty);
set => SetValue(PasswordCharProperty, value);
}
/// <summary>
/// Gets or sets the character used to represent the absence of user input in MaskedTextBox.
/// </summary>
public char PromptChar
{
get => GetValue(PromptCharProperty);
set => SetValue(PromptCharProperty, value);
}
/// <summary>
/// Gets or sets a value indicating if selected characters should be reset when the prompt character is pressed.
/// </summary>
public bool ResetOnPrompt
{
get => _resetOnPrompt;
set
{
SetAndRaise(ResetOnPromptProperty, ref _resetOnPrompt, value);
if (MaskProvider != null)
{
MaskProvider.ResetOnPrompt = value;
}
}
}
/// <summary>
/// Gets or sets a value indicating if selected characters should be reset when the space character is pressed.
/// </summary>
public bool ResetOnSpace
{
get => _resetOnSpace;
set
{
SetAndRaise(ResetOnSpaceProperty, ref _resetOnSpace, value);
if (MaskProvider != null)
{
MaskProvider.ResetOnSpace = value;
}
}
}
Type IStyleable.StyleKey => typeof(TextBox);
protected override void OnGotFocus(GotFocusEventArgs e)
{
if (HidePromptOnLeave == true && MaskProvider != null)
{
Text = MaskProvider.ToDisplayString();
}
base.OnGotFocus(e);
}
protected override async void OnKeyDown(KeyEventArgs e)
{
if (MaskProvider == null)
{
base.OnKeyDown(e);
return;
}
var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
bool Match(List<KeyGesture> gestures) => gestures.Any(g => g.Matches(e));
if (Match(keymap.Paste))
{
var text = await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))).GetTextAsync();
if (text == null)
return;
foreach (var item in text)
{
var index = GetNextCharacterPosition(CaretIndex);
if (MaskProvider.InsertAt(item, index))
{
CaretIndex = ++index;
}
}
Text = MaskProvider.ToDisplayString();
e.Handled = true;
return;
}
if (e.Key != Key.Back)
{
base.OnKeyDown(e);
}
switch (e.Key)
{
case Key.Delete:
if (CaretIndex < Text.Length)
{
if (MaskProvider.RemoveAt(CaretIndex))
{
RefreshText(MaskProvider, CaretIndex);
}
e.Handled = true;
}
break;
case Key.Space:
if (!MaskProvider.ResetOnSpace || string.IsNullOrEmpty(SelectedText))
{
if (MaskProvider.InsertAt(" ", CaretIndex))
{
RefreshText(MaskProvider, CaretIndex);
}
}
e.Handled = true;
break;
case Key.Back:
if (CaretIndex > 0)
{
MaskProvider.RemoveAt(CaretIndex - 1);
}
RefreshText(MaskProvider, CaretIndex - 1);
e.Handled = true;
break;
}
}
protected override void OnLostFocus(RoutedEventArgs e)
{
if (HidePromptOnLeave == true && MaskProvider != null)
{
Text = MaskProvider.ToString(!HidePromptOnLeave, true);
}
base.OnLostFocus(e);
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
void UpdateMaskProvider()
{
MaskProvider = new MaskedTextProvider(Mask, Culture, true, PromptChar, PasswordChar, AsciiOnly) { ResetOnSpace = ResetOnSpace, ResetOnPrompt = ResetOnPrompt };
if (Text != null)
{
MaskProvider.Set(Text);
}
RefreshText(MaskProvider, 0);
}
if (change.Property == TextProperty && MaskProvider != null && _ignoreTextChanges == false)
{
if (string.IsNullOrEmpty(Text))
{
MaskProvider.Clear();
RefreshText(MaskProvider, CaretIndex);
base.OnPropertyChanged(change);
return;
}
MaskProvider.Set(Text);
RefreshText(MaskProvider, CaretIndex);
}
else if (change.Property == MaskProperty)
{
UpdateMaskProvider();
if (!string.IsNullOrEmpty(Mask))
{
foreach (var c in Mask!)
{
if (!MaskedTextProvider.IsValidMaskChar(c))
{
throw new ArgumentException("Specified mask contains characters that are not valid.");
}
}
}
}
else if (change.Property == PasswordCharProperty)
{
if (!MaskedTextProvider.IsValidPasswordChar(PasswordChar))
{
throw new ArgumentException("Specified character value is not allowed for this property.", nameof(PasswordChar));
}
if (MaskProvider != null && PasswordChar == MaskProvider.PromptChar)
{
// Prompt and password chars must be different.
throw new InvalidOperationException("PasswordChar and PromptChar values cannot be the same.");
}
if (MaskProvider != null && MaskProvider.PasswordChar != PasswordChar)
{
UpdateMaskProvider();
}
}
else if (change.Property == PromptCharProperty)
{
if (!MaskedTextProvider.IsValidInputChar(PromptChar))
{
throw new ArgumentException("Specified character value is not allowed for this property.");
}
if (PromptChar == PasswordChar)
{
throw new InvalidOperationException("PasswordChar and PromptChar values cannot be the same.");
}
if (MaskProvider != null && MaskProvider.PromptChar != PromptChar)
{
UpdateMaskProvider();
}
}
else if (change.Property == AsciiOnlyProperty && MaskProvider != null && MaskProvider.AsciiOnly != AsciiOnly
|| change.Property == CultureProperty && MaskProvider != null && !MaskProvider.Culture.Equals(Culture))
{
UpdateMaskProvider();
}
base.OnPropertyChanged(change);
}
protected override void OnTextInput(TextInputEventArgs e)
{
_ignoreTextChanges = true;
try
{
if (IsReadOnly)
{
e.Handled = true;
base.OnTextInput(e);
return;
}
if (MaskProvider == null)
{
base.OnTextInput(e);
return;
}
if ((MaskProvider.ResetOnSpace && e.Text == " " || MaskProvider.ResetOnPrompt && e.Text == MaskProvider.PromptChar.ToString()) && !string.IsNullOrEmpty(SelectedText))
{
if (SelectionStart > SelectionEnd ? MaskProvider.RemoveAt(SelectionEnd, SelectionStart - 1) : MaskProvider.RemoveAt(SelectionStart, SelectionEnd - 1))
{
SelectedText = string.Empty;
}
}
if (CaretIndex < Text.Length)
{
CaretIndex = GetNextCharacterPosition(CaretIndex);
if (MaskProvider.InsertAt(e.Text, CaretIndex))
{
CaretIndex++;
}
var nextPos = GetNextCharacterPosition(CaretIndex);
if (nextPos != 0 && CaretIndex != Text.Length)
{
CaretIndex = nextPos;
}
}
RefreshText(MaskProvider, CaretIndex);
e.Handled = true;
base.OnTextInput(e);
}
finally
{
_ignoreTextChanges = false;
}
}
private int GetNextCharacterPosition(int startPosition)
{
if (MaskProvider != null)
{
var position = MaskProvider.FindEditPositionFrom(startPosition, true);
if (CaretIndex != -1)
{
return position;
}
}
return startPosition;
}
private void RefreshText(MaskedTextProvider provider, int position)
{
if (provider != null)
{
Text = provider.ToDisplayString();
CaretIndex = position;
}
}
}
}

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

@ -275,7 +275,7 @@ namespace Avalonia.Controls.Platform
return;
}
if (item.HasSubMenu)
if (item.HasSubMenu && item.IsEffectivelyEnabled)
{
Open(item, true);
}
@ -303,7 +303,8 @@ namespace Avalonia.Controls.Platform
{
item.Parent.SelectedItem.Close();
SelectItemAndAncestors(item);
Open(item, false);
if (item.HasSubMenu)
Open(item, false);
}
else
{

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

@ -511,8 +511,8 @@ namespace Avalonia.Controls.Presenters
else if (scrollable.IsLogicalScrollEnabled)
{
Viewport = scrollable.Viewport;
Offset = scrollable.Offset;
Extent = scrollable.Extent;
Offset = scrollable.Offset;
}
}

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

@ -53,6 +53,7 @@ namespace Avalonia.Controls.Primitives
AvaloniaProperty.Register<Popup, PopupPositionerConstraintAdjustment>(
nameof(PlacementConstraintAdjustment),
PopupPositionerConstraintAdjustment.FlipX | PopupPositionerConstraintAdjustment.FlipY |
PopupPositionerConstraintAdjustment.SlideX | PopupPositionerConstraintAdjustment.SlideY |
PopupPositionerConstraintAdjustment.ResizeX | PopupPositionerConstraintAdjustment.ResizeY);
/// <summary>
@ -145,7 +146,9 @@ namespace Avalonia.Controls.Primitives
{
IsHitTestVisibleProperty.OverrideDefaultValue<Popup>(false);
ChildProperty.Changed.AddClassHandler<Popup>((x, e) => x.ChildChanged(e));
IsOpenProperty.Changed.AddClassHandler<Popup>((x, e) => x.IsOpenChanged((AvaloniaPropertyChangedEventArgs<bool>)e));
IsOpenProperty.Changed.AddClassHandler<Popup>((x, e) => x.IsOpenChanged((AvaloniaPropertyChangedEventArgs<bool>)e));
VerticalOffsetProperty.Changed.AddClassHandler<Popup>((x, _) => x.HandlePositionChange());
HorizontalOffsetProperty.Changed.AddClassHandler<Popup>((x, _) => x.HandlePositionChange());
}
/// <summary>
@ -519,6 +522,24 @@ namespace Avalonia.Controls.Primitives
base.OnDetachedFromLogicalTree(e);
Close();
}
private void HandlePositionChange()
{
if (_openState != null)
{
var placementTarget = PlacementTarget ?? this.FindLogicalAncestorOfType<IControl>();
if (placementTarget == null)
return;
_openState.PopupHost.ConfigurePosition(
placementTarget,
PlacementMode,
new Point(HorizontalOffset, VerticalOffset),
PlacementAnchor,
PlacementGravity,
PlacementConstraintAdjustment,
PlacementRect);
}
}
private static IDisposable SubscribeToEventHandler<T, TEventHandler>(T target, TEventHandler handler, Action<T, TEventHandler> subscribe, Action<T, TEventHandler> unsubscribe)
{

73
src/Avalonia.Controls/TextBox.cs

@ -145,6 +145,18 @@ namespace Avalonia.Controls
(o, v) => o.UndoLimit = v,
unsetValue: -1);
public static readonly RoutedEvent<RoutedEventArgs> CopyingToClipboardEvent =
RoutedEvent.Register<TextBox, RoutedEventArgs>(
"CopyingToClipboard", RoutingStrategies.Bubble);
public static readonly RoutedEvent<RoutedEventArgs> CuttingToClipboardEvent =
RoutedEvent.Register<TextBox, RoutedEventArgs>(
"CuttingToClipboard", RoutingStrategies.Bubble);
public static readonly RoutedEvent<RoutedEventArgs> PastingFromClipboardEvent =
RoutedEvent.Register<TextBox, RoutedEventArgs>(
"PastingFromClipboard", RoutingStrategies.Bubble);
readonly struct UndoRedoState : IEquatable<UndoRedoState>
{
public string Text { get; }
@ -500,6 +512,24 @@ namespace Avalonia.Controls
}
}
public event EventHandler<RoutedEventArgs> CopyingToClipboard
{
add => AddHandler(CopyingToClipboardEvent, value);
remove => RemoveHandler(CopyingToClipboardEvent, value);
}
public event EventHandler<RoutedEventArgs> CuttingToClipboard
{
add => AddHandler(CuttingToClipboardEvent, value);
remove => RemoveHandler(CuttingToClipboardEvent, value);
}
public event EventHandler<RoutedEventArgs> PastingFromClipboard
{
add => AddHandler(PastingFromClipboardEvent, value);
remove => RemoveHandler(PastingFromClipboardEvent, value);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
_presenter = e.NameScope.Get<TextPresenter>("PART_TextPresenter");
@ -638,27 +668,54 @@ namespace Avalonia.Controls
public async void Cut()
{
var text = GetSelection();
if (text is null) return;
if (string.IsNullOrEmpty(text))
{
return;
}
SnapshotUndoRedo();
Copy();
DeleteSelection();
var eventArgs = new RoutedEventArgs(CuttingToClipboardEvent);
RaiseEvent(eventArgs);
if (!eventArgs.Handled)
{
SnapshotUndoRedo();
await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard)))
.SetTextAsync(text);
DeleteSelection();
}
}
public async void Copy()
{
var text = GetSelection();
if (text is null) return;
if (string.IsNullOrEmpty(text))
{
return;
}
await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard)))
.SetTextAsync(text);
var eventArgs = new RoutedEventArgs(CopyingToClipboardEvent);
RaiseEvent(eventArgs);
if (!eventArgs.Handled)
{
await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard)))
.SetTextAsync(text);
}
}
public async void Paste()
{
var eventArgs = new RoutedEventArgs(PastingFromClipboardEvent);
RaiseEvent(eventArgs);
if (eventArgs.Handled)
{
return;
}
var text = await ((IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))).GetTextAsync();
if (text is null) return;
if (string.IsNullOrEmpty(text))
{
return;
}
SnapshotUndoRedo();
HandleTextInput(text);

4
src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs

@ -30,13 +30,13 @@ namespace Avalonia.Dialogs
}
else
{
using (Process process = Process.Start(new ProcessStartInfo
using Process process = Process.Start(new ProcessStartInfo
{
FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? url : "open",
Arguments = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? $"{url}" : "",
CreateNoWindow = true,
UseShellExecute = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
}));
});
}
}

4
src/Avalonia.Dialogs/ManagedFileChooser.cs

@ -1,13 +1,11 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml;
namespace Avalonia.Dialogs
{
@ -35,7 +33,9 @@ namespace Avalonia.Dialogs
if (_quickLinksRoot != null)
{
var isQuickLink = _quickLinksRoot.IsLogicalAncestorOf(e.Source as Control);
#pragma warning disable CS0618 // Type or member is obsolete
if (e.ClickCount == 2 || isQuickLink)
#pragma warning restore CS0618 // Type or member is obsolete
{
if (model.ItemType == ManagedFileChooserItemType.File)
{

2
src/Avalonia.Dialogs/ManagedFileChooserSources.cs

@ -67,7 +67,7 @@ namespace Avalonia.Dialogs
{
Directory.GetFiles(x.VolumePath);
}
catch (Exception _)
catch (Exception)
{
return null;
}

2
src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@ -47,6 +47,8 @@ namespace Avalonia.Headless
}
public IStreamGeometryImpl CreateStreamGeometry() => new HeadlessStreamingGeometryStub();
public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children) => throw new NotImplementedException();
public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => throw new NotImplementedException();
public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces) => new HeadlessRenderTarget();

5
src/Avalonia.Input/AccessKeyHandler.cs

@ -157,10 +157,9 @@ namespace Avalonia.Input
_restoreFocusElement?.Focus();
_restoreFocusElement = null;
e.Handled = true;
}
// We always handle the Alt key.
e.Handled = true;
}
else if (_altIsDown)
{

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

@ -101,6 +101,7 @@
Grid.Column="1"
Background="Transparent"
Content="{TemplateBinding Content}"
ContentTemplate="{Binding $parent[Expander].HeaderTemplate}"
VerticalAlignment="Center"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"

4
src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml

@ -75,8 +75,6 @@
</Grid.ColumnDefinitions>
<ContentPresenter Name="PART_IconPresenter"
Content="{TemplateBinding Icon}"
Width="16"
Height="16"
Margin="{DynamicResource MenuIconPresenterMargin}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
@ -199,6 +197,8 @@
</Style>
<Style Selector="MenuItem /template/ ContentPresenter#PART_IconPresenter">
<Setter Property="Width" Value="16" />
<Setter Property="Height" Value="16" />
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="MenuItem:icon /template/ ContentPresenter#PART_IconPresenter">

2
src/Avalonia.Themes.Fluent/Controls/NotificationCard.xaml

@ -17,7 +17,7 @@
<Setter Property="Template">
<ControlTemplate>
<LayoutTransformControl Name="PART_LayoutTransformControl" UseRenderTransform="True">
<Border CornerRadius="{DynamicResource ControlCornerRadius}" BoxShadow="0 6 8 0 #4F000000" Margin="5 5 5 10">
<Border CornerRadius="{TemplateBinding CornerRadius}" BoxShadow="0 6 8 0 #4F000000" Margin="5 5 5 10">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"

6
src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml

@ -23,11 +23,11 @@
<Border x:Name="ProgressBarRoot" ClipToBounds="True" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}">
<Panel>
<Panel x:Name="DeterminateRoot">
<Border CornerRadius="{DynamicResource ControlCornerRadius}" x:Name="PART_Indicator" Margin="{TemplateBinding Padding}" Background="{TemplateBinding Foreground}" />
<Border CornerRadius="{TemplateBinding CornerRadius}" x:Name="PART_Indicator" Margin="{TemplateBinding Padding}" Background="{TemplateBinding Foreground}" />
</Panel>
<Panel x:Name="IndeterminateRoot">
<Border x:Name="IndeterminateProgressBarIndicator" CornerRadius="{DynamicResource ControlCornerRadius}" Margin="{TemplateBinding Padding}" Background="{TemplateBinding Foreground}" />
<Border x:Name="IndeterminateProgressBarIndicator2" CornerRadius="{DynamicResource ControlCornerRadius}" Margin="{TemplateBinding Padding}" Background="{TemplateBinding Foreground}" />
<Border x:Name="IndeterminateProgressBarIndicator" CornerRadius="{TemplateBinding CornerRadius}" Margin="{TemplateBinding Padding}" Background="{TemplateBinding Foreground}" />
<Border x:Name="IndeterminateProgressBarIndicator2" CornerRadius="{TemplateBinding CornerRadius}" Margin="{TemplateBinding Padding}" Background="{TemplateBinding Foreground}" />
</Panel>
</Panel>
</Border>

4
src/Avalonia.Visuals/ApiCompatBaseline.txt

@ -67,6 +67,8 @@ InterfacesShouldHaveSameMembers : Interface member 'public System.Double Avaloni
InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IGeometryImpl.TryGetPointAndTangentAtDistance(System.Double, Avalonia.Point, Avalonia.Point)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IGeometryImpl.TryGetPointAtDistance(System.Double, Avalonia.Point)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public System.Boolean Avalonia.Platform.IGeometryImpl.TryGetSegment(System.Double, System.Double, System.Boolean, Avalonia.Platform.IGeometryImpl)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGeometryImpl Avalonia.Platform.IPlatformRenderInterface.CreateCombinedGeometry(Avalonia.Media.GeometryCombineMode, Avalonia.Media.Geometry, Avalonia.Media.Geometry)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGeometryImpl Avalonia.Platform.IPlatformRenderInterface.CreateGeometryGroup(Avalonia.Media.FillRule, System.Collections.Generic.IReadOnlyList<Avalonia.Media.Geometry>)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun, System.Double)' is present in the contract but not in the implementation.
MembersMustExist : Member 'public Avalonia.Platform.IGlyphRunImpl Avalonia.Platform.IPlatformRenderInterface.CreateGlyphRun(Avalonia.Media.GlyphRun, System.Double)' does not exist in the implementation but it does exist in the contract.
@ -74,4 +76,4 @@ InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWr
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmap(System.String)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmapToHeight(System.IO.Stream, System.Int32, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' is present in the implementation but not in the contract.
InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.IWriteableBitmapImpl Avalonia.Platform.IPlatformRenderInterface.LoadWriteableBitmapToWidth(System.IO.Stream, System.Int32, Avalonia.Visuals.Media.Imaging.BitmapInterpolationMode)' is present in the implementation but not in the contract.
Total Issues: 75
Total Issues: 77

170
src/Avalonia.Visuals/Media/CombinedGeometry.cs

@ -0,0 +1,170 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Platform;
#nullable enable
namespace Avalonia.Media
{
public enum GeometryCombineMode
{
/// <summary>
/// The two regions are combined by taking the union of both. The resulting geometry is
/// geometry A + geometry B.
/// </summary>
Union,
/// <summary>
/// The two regions are combined by taking their intersection. The new area consists of the
/// overlapping region between the two geometries.
/// </summary>
Intersect,
/// <summary>
/// The two regions are combined by taking the area that exists in the first region but not
/// the second and the area that exists in the second region but not the first. The new
/// region consists of (A-B) + (B-A), where A and B are geometries.
/// </summary>
Xor,
/// <summary>
/// The second region is excluded from the first. Given two geometries, A and B, the area of
/// geometry B is removed from the area of geometry A, producing a region that is A-B.
/// </summary>
Exclude,
}
/// <summary>
/// Represents a 2-D geometric shape defined by the combination of two Geometry objects.
/// </summary>
public class CombinedGeometry : Geometry
{
/// <summary>
/// Defines the <see cref="Geometry1"/> property.
/// </summary>
public static readonly StyledProperty<Geometry?> Geometry1Property =
AvaloniaProperty.Register<CombinedGeometry, Geometry?>(nameof(Geometry1));
/// <summary>
/// Defines the <see cref="Geometry2"/> property.
/// </summary>
public static readonly StyledProperty<Geometry?> Geometry2Property =
AvaloniaProperty.Register<CombinedGeometry, Geometry?>(nameof(Geometry2));
/// <summary>
/// Defines the <see cref="GeometryCombineMode"/> property.
/// </summary>
public static readonly StyledProperty<GeometryCombineMode> GeometryCombineModeProperty =
AvaloniaProperty.Register<CombinedGeometry, GeometryCombineMode>(nameof(GeometryCombineMode));
/// <summary>
/// Initializes a new instance of the <see cref="CombinedGeometry"/> class.
/// </summary>
public CombinedGeometry()
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CombinedGeometry"/> class with the
/// specified <see cref="Geometry"/> objects.
/// </summary>
/// <param name="geometry1">The first geometry to combine.</param>
/// <param name="geometry2">The second geometry to combine.</param>
public CombinedGeometry(Geometry geometry1, Geometry geometry2)
{
Geometry1 = geometry1;
Geometry2 = geometry2;
}
/// <summary>
/// Initializes a new instance of the <see cref="CombinedGeometry"/> class with the
/// specified <see cref="Geometry"/> objects and <see cref="GeometryCombineMode"/>.
/// </summary>
/// <param name="combineMode">The method by which geometry1 and geometry2 are combined.</param>
/// <param name="geometry1">The first geometry to combine.</param>
/// <param name="geometry2">The second geometry to combine.</param>
public CombinedGeometry(GeometryCombineMode combineMode, Geometry? geometry1, Geometry? geometry2)
{
Geometry1 = geometry1;
Geometry2 = geometry2;
GeometryCombineMode = combineMode;
}
/// <summary>
/// Initializes a new instance of the <see cref="CombinedGeometry"/> class with the
/// specified <see cref="Geometry"/> objects, <see cref="GeometryCombineMode"/> and
/// <see cref="Transform"/>.
/// </summary>
/// <param name="combineMode">The method by which geometry1 and geometry2 are combined.</param>
/// <param name="geometry1">The first geometry to combine.</param>
/// <param name="geometry2">The second geometry to combine.</param>
/// <param name="transform">The transform applied to the geometry.</param>
public CombinedGeometry(
GeometryCombineMode combineMode,
Geometry? geometry1,
Geometry? geometry2,
Transform? transform)
{
Geometry1 = geometry1;
Geometry2 = geometry2;
GeometryCombineMode = combineMode;
Transform = transform;
}
/// <summary>
/// Gets or sets the first <see cref="Geometry"/> object of this
/// <see cref="CombinedGeometry"/> object.
/// </summary>
public Geometry? Geometry1
{
get => GetValue(Geometry1Property);
set => SetValue(Geometry1Property, value);
}
/// <summary>
/// Gets or sets the second <see cref="Geometry"/> object of this
/// <see cref="CombinedGeometry"/> object.
/// </summary>
public Geometry? Geometry2
{
get => GetValue(Geometry2Property);
set => SetValue(Geometry2Property, value);
}
/// <summary>
/// Gets or sets the method by which the two geometries (specified by the
/// <see cref="Geometry1"/> and <see cref="Geometry2"/> properties) are combined. The
/// default value is <see cref="GeometryCombineMode.Union"/>.
/// </summary>
public GeometryCombineMode GeometryCombineMode
{
get => GetValue(GeometryCombineModeProperty);
set => SetValue(GeometryCombineModeProperty, value);
}
public override Geometry Clone()
{
return new CombinedGeometry(GeometryCombineMode, Geometry1, Geometry2, Transform);
}
protected override IGeometryImpl? CreateDefiningGeometry()
{
var g1 = Geometry1;
var g2 = Geometry2;
if (g1 is object && g2 is object)
{
var factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
return factory.CreateCombinedGeometry(GeometryCombineMode, g1, g2);
}
else if (GeometryCombineMode == GeometryCombineMode.Intersect)
return null;
else if (g1 is object)
return g1.PlatformImpl;
else if (g2 is object)
return g2.PlatformImpl;
else
return null;
}
}
}

2
src/Avalonia.Visuals/Media/FormattedText.cs

@ -200,7 +200,7 @@ namespace Avalonia.Media
private void Set<T>(ref T field, T value)
{
if (field != null && field.Equals(value))
if (EqualityComparer<T>.Default.Equals(field, value))
{
return;
}

37
src/Avalonia.Visuals/Media/GeometryCollection.cs

@ -0,0 +1,37 @@
using System.Collections;
using System.Collections.Generic;
using Avalonia.Animation;
#nullable enable
namespace Avalonia.Media
{
public class GeometryCollection : Animatable, IList<Geometry>, IReadOnlyList<Geometry>
{
private List<Geometry> _inner;
public GeometryCollection() => _inner = new List<Geometry>();
public GeometryCollection(IEnumerable<Geometry> collection) => _inner = new List<Geometry>(collection);
public GeometryCollection(int capacity) => _inner = new List<Geometry>(capacity);
public Geometry this[int index]
{
get => _inner[index];
set => _inner[index] = value;
}
public int Count => _inner.Count;
public bool IsReadOnly => false;
public void Add(Geometry item) => _inner.Add(item);
public void Clear() => _inner.Clear();
public bool Contains(Geometry item) => _inner.Contains(item);
public void CopyTo(Geometry[] array, int arrayIndex) => _inner.CopyTo(array, arrayIndex);
public IEnumerator<Geometry> GetEnumerator() => _inner.GetEnumerator();
public int IndexOf(Geometry item) => _inner.IndexOf(item);
public void Insert(int index, Geometry item) => _inner.Insert(index, item);
public bool Remove(Geometry item) => _inner.Remove(item);
public void RemoveAt(int index) => _inner.RemoveAt(index);
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

80
src/Avalonia.Visuals/Media/GeometryGroup.cs

@ -0,0 +1,80 @@
using System.Collections.Generic;
using System.Linq;
using Avalonia.Metadata;
using Avalonia.Platform;
#nullable enable
namespace Avalonia.Media
{
/// <summary>
/// Represents a composite geometry, composed of other <see cref="Geometry"/> objects.
/// </summary>
public class GeometryGroup : Geometry
{
public static readonly DirectProperty<GeometryGroup, GeometryCollection?> ChildrenProperty =
AvaloniaProperty.RegisterDirect<GeometryGroup, GeometryCollection?> (
nameof(Children),
o => o.Children,
(o, v) => o.Children = v);
public static readonly StyledProperty<FillRule> FillRuleProperty =
AvaloniaProperty.Register<GeometryGroup, FillRule>(nameof(FillRule));
private GeometryCollection? _children;
private bool _childrenSet;
/// <summary>
/// Gets or sets the collection that contains the child geometries.
/// </summary>
[Content]
public GeometryCollection? Children
{
get => _children ??= (!_childrenSet ? new GeometryCollection() : null);
set
{
SetAndRaise(ChildrenProperty, ref _children, value);
_childrenSet = true;
}
}
/// <summary>
/// Gets or sets how the intersecting areas of the objects contained in this
/// <see cref="GeometryGroup"/> are combined. The default is <see cref="FillRule.EvenOdd"/>.
/// </summary>
public FillRule FillRule
{
get => GetValue(FillRuleProperty);
set => SetValue(FillRuleProperty, value);
}
public override Geometry Clone()
{
var result = new GeometryGroup { FillRule = FillRule, Transform = Transform };
if (_children?.Count > 0)
result.Children = new GeometryCollection(_children);
return result;
}
protected override IGeometryImpl? CreateDefiningGeometry()
{
if (_children?.Count > 0)
{
var factory = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
return factory.CreateGeometryGroup(FillRule, _children);
}
return null;
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(change);
if (change.Property == ChildrenProperty || change.Property == FillRuleProperty)
{
InvalidateGeometry();
}
}
}
}

10
src/Avalonia.Visuals/Media/PathMarkupParser.cs

@ -496,12 +496,18 @@ namespace Avalonia.Media
private bool ReadBool(ref ReadOnlySpan<char> span)
{
if (!ReadArgument(ref span, out var boolValue) || boolValue.Length != 1)
span = SkipWhitespace(span);
if (span.IsEmpty)
{
throw new InvalidDataException("Invalid bool rule.");
}
switch (boolValue[0])
var c = span[0];
span = span.Slice(1);
switch (c)
{
case '0':
return false;

52
src/Avalonia.Visuals/Media/RotateTransform.cs

@ -14,6 +14,18 @@ namespace Avalonia.Media
public static readonly StyledProperty<double> AngleProperty =
AvaloniaProperty.Register<RotateTransform, double>(nameof(Angle));
/// <summary>
/// Defines the <see cref="CenterX"/> property.
/// </summary>
public static readonly StyledProperty<double> CenterXProperty =
AvaloniaProperty.Register<RotateTransform, double>(nameof(CenterX));
/// <summary>
/// Defines the <see cref="CenterY"/> property.
/// </summary>
public static readonly StyledProperty<double> CenterYProperty =
AvaloniaProperty.Register<RotateTransform, double>(nameof(CenterY));
/// <summary>
/// Initializes a new instance of the <see cref="RotateTransform"/> class.
/// </summary>
@ -32,18 +44,52 @@ namespace Avalonia.Media
Angle = angle;
}
/// <summary>
/// Initializes a new instance of the <see cref="RotateTransform"/> class.
/// </summary>
/// <param name="angle">The angle, in degrees.</param>
/// <param name="centerX">The x-coordinate of the center point for the rotation.</param>
/// <param name="centerY">The y-coordinate of the center point for the rotation.</param>
public RotateTransform(double angle, double centerX, double centerY)
: this()
{
Angle = angle;
CenterX = centerX;
CenterY = centerY;
}
/// <summary>
/// Gets or sets the angle of rotation, in degrees.
/// </summary>
public double Angle
{
get { return GetValue(AngleProperty); }
set { SetValue(AngleProperty, value); }
get => GetValue(AngleProperty);
set => SetValue(AngleProperty, value);
}
/// <summary>
/// Gets or sets the x-coordinate of the rotation center point. The default is 0.
/// </summary>
public double CenterX
{
get => GetValue(CenterXProperty);
set => SetValue(CenterXProperty, value);
}
/// <summary>
/// Gets or sets the y-coordinate of the rotation center point. The default is 0.
/// </summary>
public double CenterY
{
get => GetValue(CenterYProperty);
set => SetValue(CenterYProperty, value);
}
/// <summary>
/// Gets the transform's <see cref="Matrix"/>.
/// </summary>
public override Matrix Value => Matrix.CreateRotation(Matrix.ToRadians(Angle));
public override Matrix Value => Matrix.CreateTranslation(-CenterX, -CenterY) *
Matrix.CreateRotation(Matrix.ToRadians(Angle)) *
Matrix.CreateTranslation(CenterX, CenterY);
}
}

17
src/Avalonia.Visuals/Platform/IPlatformRenderInterface.cs

@ -59,6 +59,23 @@ namespace Avalonia.Platform
/// <returns>An <see cref="IStreamGeometryImpl"/>.</returns>
IStreamGeometryImpl CreateStreamGeometry();
/// <summary>
/// Creates a geometry group implementation.
/// </summary>
/// <param name="fillRule">The fill rule.</param>
/// <param name="children">The geometries to group.</param>
/// <returns>A combined geometry.</returns>
IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children);
/// <summary>
/// Creates a geometry group implementation.
/// </summary>
/// <param name="combineMode">The combine mode</param>
/// <param name="g1">The first geometry.</param>
/// <param name="g2">The second geometry.</param>
/// <returns>A combined geometry.</returns>
IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2);
/// <summary>
/// Creates a renderer.
/// </summary>

33
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

@ -35,6 +35,8 @@ namespace Avalonia.Rendering
private IRef<IDrawOperation> _currentDraw;
private readonly IDeferredRendererLock _lock;
private readonly object _sceneLock = new object();
private readonly object _startStopLock = new object();
private readonly object _renderLoopIsRenderingLock = new object();
private readonly Action _updateSceneIfNeededDelegate;
/// <summary>
@ -139,6 +141,8 @@ namespace Avalonia.Rendering
}
Stop();
// Wait for any in-progress rendering to complete
lock(_renderLoopIsRenderingLock){}
DisposeRenderTarget();
}
@ -233,20 +237,26 @@ namespace Avalonia.Rendering
/// <inheritdoc/>
public void Start()
{
if (!_running && _renderLoop != null)
lock (_startStopLock)
{
_renderLoop.Add(this);
_running = true;
if (!_running && _renderLoop != null)
{
_renderLoop.Add(this);
_running = true;
}
}
}
/// <inheritdoc/>
public void Stop()
{
if (_running && _renderLoop != null)
lock (_startStopLock)
{
_renderLoop.Remove(this);
_running = false;
if (_running && _renderLoop != null)
{
_renderLoop.Remove(this);
_running = false;
}
}
}
@ -255,7 +265,16 @@ namespace Avalonia.Rendering
void IRenderLoopTask.Update(TimeSpan time) => UpdateScene();
void IRenderLoopTask.Render() => Render(false);
void IRenderLoopTask.Render()
{
lock (_renderLoopIsRenderingLock)
{
lock(_startStopLock)
if(!_running)
return;
Render(false);
}
}
/// <inheritdoc/>
Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush)

7
src/Avalonia.Visuals/Visual.cs

@ -489,11 +489,8 @@ namespace Avalonia
protected internal sealed override void LogBindingError(AvaloniaProperty property, Exception e)
{
// Don't log a binding error unless the control is attached to a logical or visual tree.
// In theory this should only need to check for logical tree attachment, but in practise
// due to ContentControlMixin only taking effect when the template has finished being
// applied, some controls are attached to the visual tree before the logical tree.
if (((ILogical)this).IsAttachedToLogicalTree || ((IVisual)this).IsAttachedToVisualTree)
// Don't log a binding error unless the control is attached to a logical tree.
if (((ILogical)this).IsAttachedToLogicalTree)
{
if (e is BindingChainException b &&
string.IsNullOrEmpty(b.ExpressionErrorPoint) &&

8
src/Avalonia.X11/X11Window.cs

@ -192,11 +192,6 @@ namespace Avalonia.X11
if (platform.Options.UseDBusMenu)
NativeMenuExporter = DBusMenuExporter.TryCreate(_handle);
NativeControlHost = new X11NativeControlHost(_platform, this);
DispatcherTimer.Run(() =>
{
Paint?.Invoke(default);
return _handle != IntPtr.Zero;
}, TimeSpan.FromMilliseconds(100));
InitializeIme();
}
@ -805,13 +800,14 @@ namespace Avalonia.X11
if (_handle != IntPtr.Zero)
{
XDestroyWindow(_x11.Display, _handle);
_platform.Windows.Remove(_handle);
_platform.XI2?.OnWindowDestroyed(_handle);
var handle = _handle;
_handle = IntPtr.Zero;
Closed?.Invoke();
_mouse.Dispose();
_touch.Dispose();
XDestroyWindow(_x11.Display, handle);
}
if (_useRenderWindow && _renderHandle != IntPtr.Zero)

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

@ -30,10 +30,9 @@ namespace Avalonia.LinuxFramebuffer
public IRenderer CreateRenderer(IRenderRoot root)
{
return new DeferredRenderer(root, AvaloniaLocator.Current.GetService<IRenderLoop>())
{
};
var factory = AvaloniaLocator.Current.GetService<IRendererFactory>();
var renderLoop = AvaloniaLocator.Current.GetService<IRenderLoop>();
return factory?.Create(root, renderLoop) ?? new DeferredRenderer(root, renderLoop);
}
public void Dispose()
@ -41,7 +40,7 @@ namespace Avalonia.LinuxFramebuffer
throw new NotSupportedException();
}
public void Invalidate(Rect rect)
{
}

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

@ -37,16 +37,17 @@ namespace Avalonia.LinuxFramebuffer
Threading = new InternalPlatformThreadingInterface();
if (_fb is IGlOutputBackend gl)
AvaloniaLocator.CurrentMutable.Bind<IPlatformOpenGlInterface>().ToConstant(gl.PlatformOpenGlInterface);
var opts = AvaloniaLocator.Current.GetService<LinuxFramebufferPlatformOptions>();
AvaloniaLocator.CurrentMutable
.Bind<IPlatformThreadingInterface>().ToConstant(Threading)
.Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))
.Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(opts?.Fps ?? 60))
.Bind<IRenderLoop>().ToConstant(new RenderLoop())
.Bind<ICursorFactory>().ToTransient<CursorFactoryStub>()
.Bind<IKeyboardDevice>().ToConstant(new KeyboardDevice())
.Bind<IPlatformSettings>().ToSingleton<PlatformSettings>()
.Bind<IRenderLoop>().ToConstant(new RenderLoop())
.Bind<PlatformHotkeyConfiguration>().ToSingleton<PlatformHotkeyConfiguration>();
}

14
src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatformOptions.cs

@ -0,0 +1,14 @@
namespace Avalonia.LinuxFramebuffer
{
/// <summary>
/// Platform-specific options which apply to the Linux framebuffer.
/// </summary>
public class LinuxFramebufferPlatformOptions
{
/// <summary>
/// Gets or sets the number of frames per second at which the renderer should run.
/// Default 60.
/// </summary>
public int Fps { get; set; } = 60;
}
}

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

@ -1,46 +0,0 @@
using System;
using System.Runtime.InteropServices;
using Avalonia.Platform;
namespace Avalonia.LinuxFramebuffer
{
unsafe class LockedFramebuffer : ILockedFramebuffer
{
private readonly int _fb;
private readonly fb_fix_screeninfo _fixedInfo;
private fb_var_screeninfo _varInfo;
private readonly IntPtr _address;
public LockedFramebuffer(int fb, fb_fix_screeninfo fixedInfo, fb_var_screeninfo varInfo, IntPtr address, Vector dpi)
{
_fb = fb;
_fixedInfo = fixedInfo;
_varInfo = varInfo;
_address = address;
Dpi = dpi;
//Use double buffering to avoid flicker
Address = Marshal.AllocHGlobal(RowBytes * Size.Height);
}
void VSync()
{
NativeUnsafeMethods.ioctl(_fb, FbIoCtl.FBIO_WAITFORVSYNC, null);
}
public void Dispose()
{
VSync();
NativeUnsafeMethods.memcpy(_address, Address, new IntPtr(RowBytes * Size.Height));
Marshal.FreeHGlobal(Address);
Address = IntPtr.Zero;
}
public IntPtr Address { get; private set; }
public PixelSize Size => new PixelSize((int)_varInfo.xres, (int) _varInfo.yres);
public int RowBytes => (int) _fixedInfo.line_length;
public Vector Dpi { get; }
public PixelFormat Format => _varInfo.bits_per_pixel == 16 ? PixelFormat.Rgb565 : _varInfo.blue.offset == 16 ? PixelFormat.Rgba8888 : PixelFormat.Bgra8888;
}
}

70
src/Linux/Avalonia.LinuxFramebuffer/Output/FbDevBackBuffer.cs

@ -0,0 +1,70 @@
using System;
using System.Runtime.InteropServices;
using System.Threading;
using Avalonia.Platform;
namespace Avalonia.LinuxFramebuffer.Output
{
internal unsafe class FbDevBackBuffer : IDisposable
{
private readonly int _fb;
private readonly fb_fix_screeninfo _fixedInfo;
private readonly fb_var_screeninfo _varInfo;
private readonly IntPtr _targetAddress;
private readonly object _lock = new object();
public FbDevBackBuffer(int fb, fb_fix_screeninfo fixedInfo, fb_var_screeninfo varInfo, IntPtr targetAddress)
{
_fb = fb;
_fixedInfo = fixedInfo;
_varInfo = varInfo;
_targetAddress = targetAddress;
Address = Marshal.AllocHGlobal(RowBytes * Size.Height);
}
public void Dispose()
{
if (Address != IntPtr.Zero)
{
Marshal.FreeHGlobal(Address);
Address = IntPtr.Zero;
}
}
public ILockedFramebuffer Lock(Vector dpi)
{
Monitor.Enter(_lock);
try
{
return new LockedFramebuffer(Address,
new PixelSize((int)_varInfo.xres, (int)_varInfo.yres),
(int)_fixedInfo.line_length, dpi,
_varInfo.bits_per_pixel == 16 ? PixelFormat.Rgb565
: _varInfo.blue.offset == 16 ? PixelFormat.Rgba8888
: PixelFormat.Bgra8888,
() =>
{
try
{
NativeUnsafeMethods.ioctl(_fb, FbIoCtl.FBIO_WAITFORVSYNC, null);
NativeUnsafeMethods.memcpy(_targetAddress, Address, new IntPtr(RowBytes * Size.Height));
}
finally
{
Monitor.Exit(_lock);
}
});
}
catch
{
Monitor.Exit(_lock);
throw;
}
}
public IntPtr Address { get; private set; }
public PixelSize Size => new PixelSize((int)_varInfo.xres, (int) _varInfo.yres);
public int RowBytes => (int) _fixedInfo.line_length;
}
}

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

@ -14,6 +14,7 @@ namespace Avalonia.LinuxFramebuffer
private fb_var_screeninfo _varInfo;
private IntPtr _mappedLength;
private IntPtr _mappedAddress;
private FbDevBackBuffer _backBuffer;
public double Scaling { get; set; }
/// <summary>
@ -146,7 +147,9 @@ namespace Avalonia.LinuxFramebuffer
{
if (_fd <= 0)
throw new ObjectDisposedException("LinuxFramebuffer");
return new LockedFramebuffer(_fd, _fixedInfo, _varInfo, _mappedAddress, new Vector(96, 96) * Scaling);
return (_backBuffer ??=
new FbDevBackBuffer(_fd, _fixedInfo, _varInfo, _mappedAddress))
.Lock(new Vector(96, 96) * Scaling);
}
@ -165,6 +168,8 @@ namespace Avalonia.LinuxFramebuffer
public void Dispose()
{
_backBuffer?.Dispose();
_backBuffer = null;
ReleaseUnmanagedResources();
GC.SuppressFinalize(this);
}

2
src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github

@ -1 +1 @@
Subproject commit 9e90d34e97c766ba8dcb70128147fcded65d195a
Subproject commit f4ac681b91a9dc7a7a095d1050a683de23d86b72

23
src/Markup/Avalonia.Markup.Xaml/Extensions.cs

@ -2,7 +2,9 @@ using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Markup.Xaml.XamlIl.Runtime;
using Avalonia.Styling;
namespace Avalonia.Markup.Xaml
{
@ -32,5 +34,26 @@ namespace Avalonia.Markup.Xaml
string name = string.IsNullOrEmpty(namespacePrefix) ? type : $"{namespacePrefix}:{type}";
return tr?.Resolve(name);
}
public static object GetDefaultAnchor(this IServiceProvider provider)
{
// If the target is not a control, so we need to find an anchor that will let us look
// up named controls and style resources. First look for the closest IControl in
// the context.
object anchor = provider.GetFirstParent<IControl>();
if (anchor is null)
{
// Try to find IDataContextProvider, this was added to allow us to find
// a datacontext for Application class when using NativeMenuItems.
anchor = provider.GetFirstParent<IDataContextProvider>();
}
// If a control was not found, then try to find the highest-level style as the XAML
// file could be a XAML file containing only styles.
return anchor ??
provider.GetService<IRootObjectProvider>()?.RootObject as IStyle ??
provider.GetLastParent<IStyle>();
}
}
}

18
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs

@ -1,7 +1,5 @@
using System;
using Avalonia.Data;
using Avalonia.Controls;
using Avalonia.Styling;
using Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings;
using Avalonia.Data.Core;
using Avalonia.Markup.Parsers;
@ -33,24 +31,10 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
Priority = Priority,
StringFormat = StringFormat,
Source = Source,
DefaultAnchor = new WeakReference(GetDefaultAnchor(provider))
DefaultAnchor = new WeakReference(provider.GetDefaultAnchor())
};
}
private static object GetDefaultAnchor(IServiceProvider provider)
{
// If the target is not a control, so we need to find an anchor that will let us look
// up named controls and style resources. First look for the closest IControl in
// the context.
object anchor = provider.GetFirstParent<IControl>();
// If a control was not found, then try to find the highest-level style as the XAML
// file could be a XAML file containing only styles.
return anchor ??
provider.GetService<IRootObjectProvider>()?.RootObject as IStyle ??
provider.GetLastParent<IStyle>();
}
protected override ExpressionObserver CreateExpressionObserver(IAvaloniaObject target, AvaloniaProperty targetProperty, object anchor, bool enableDataValidation)
{
if (Source != null)

23
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ReflectionBindingExtension.cs

@ -37,33 +37,12 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
Source = Source,
StringFormat = StringFormat,
RelativeSource = RelativeSource,
DefaultAnchor = new WeakReference(GetDefaultAnchor(descriptorContext)),
DefaultAnchor = new WeakReference(descriptorContext.GetDefaultAnchor()),
TargetNullValue = TargetNullValue,
NameScope = new WeakReference<INameScope>(serviceProvider.GetService<INameScope>())
};
}
private static object GetDefaultAnchor(IServiceProvider context)
{
// If the target is not a control, so we need to find an anchor that will let us look
// up named controls and style resources. First look for the closest IControl in
// the context.
object anchor = context.GetFirstParent<IControl>();
if(anchor is null)
{
// Try to find IDataContextProvider, this was added to allow us to find
// a datacontext for Application class when using NativeMenuItems.
anchor = context.GetFirstParent<IDataContextProvider>();
}
// If a control was not found, then try to find the highest-level style as the XAML
// file could be a XAML file containing only styles.
return anchor ??
context.GetService<IRootObjectProvider>()?.RootObject as IStyle ??
context.GetLastParent<IStyle>();
}
public IValueConverter Converter { get; set; }
public object ConverterParameter { get; set; }

35
src/Skia/Avalonia.Skia/CombinedGeometryImpl.cs

@ -0,0 +1,35 @@
using System.Collections.Generic;
using Avalonia.Media;
using SkiaSharp;
#nullable enable
namespace Avalonia.Skia
{
/// <summary>
/// A Skia implementation of a <see cref="Avalonia.Media.GeometryGroup"/>.
/// </summary>
internal class CombinedGeometryImpl : GeometryImpl
{
public CombinedGeometryImpl(GeometryCombineMode combineMode, Geometry g1, Geometry g2)
{
var path1 = ((GeometryImpl)g1.PlatformImpl).EffectivePath;
var path2 = ((GeometryImpl)g2.PlatformImpl).EffectivePath;
var op = combineMode switch
{
GeometryCombineMode.Intersect => SKPathOp.Intersect,
GeometryCombineMode.Xor => SKPathOp.Xor,
GeometryCombineMode.Exclude => SKPathOp.Difference,
_ => SKPathOp.Union,
};
var path = path1.Op(path2, op);
EffectivePath = path;
Bounds = path.Bounds.ToAvaloniaRect();
}
public override Rect Bounds { get; }
public override SKPath EffectivePath { get; }
}
}

47
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -164,7 +164,7 @@ namespace Avalonia.Skia
/// <inheritdoc />
public void DrawLine(IPen pen, Point p1, Point p2)
{
using (var paint = CreatePaint(_strokePaint, pen, new Rect(p1, p2).Normalize()))
using (var paint = CreatePaint(_strokePaint, pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y))))
{
if (paint.Paint is object)
{
@ -177,10 +177,10 @@ namespace Avalonia.Skia
public void DrawGeometry(IBrush brush, IPen pen, IGeometryImpl geometry)
{
var impl = (GeometryImpl) geometry;
var rect = geometry.Bounds;
var size = geometry.Bounds.Size;
using (var fill = brush != null ? CreatePaint(_fillPaint, brush, rect) : default(PaintWrapper))
using (var stroke = pen?.Brush != null ? CreatePaint(_strokePaint, pen, rect) : default(PaintWrapper))
using (var fill = brush != null ? CreatePaint(_fillPaint, brush, size) : default(PaintWrapper))
using (var stroke = pen?.Brush != null ? CreatePaint(_strokePaint, pen, size) : default(PaintWrapper))
{
if (fill.Paint != null)
{
@ -354,7 +354,7 @@ namespace Avalonia.Skia
if (brush != null)
{
using (var paint = CreatePaint(_fillPaint, brush, rect.Rect))
using (var paint = CreatePaint(_fillPaint, brush, rect.Rect.Size))
{
if (isRounded)
{
@ -397,7 +397,7 @@ namespace Avalonia.Skia
if (pen?.Brush != null)
{
using (var paint = CreatePaint(_strokePaint, pen, rect.Rect))
using (var paint = CreatePaint(_strokePaint, pen, rect.Rect.Size))
{
if (paint.Paint is object)
{
@ -417,7 +417,7 @@ namespace Avalonia.Skia
/// <inheritdoc />
public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
{
using (var paint = CreatePaint(_fillPaint, foreground, text.Bounds))
using (var paint = CreatePaint(_fillPaint, foreground, text.Bounds.Size))
{
var textImpl = (FormattedTextImpl) text;
textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, _canTextUseLcdRendering);
@ -427,7 +427,7 @@ namespace Avalonia.Skia
/// <inheritdoc />
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun)
{
using (var paintWrapper = CreatePaint(_fillPaint, foreground, new Rect(glyphRun.Size)))
using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Size))
{
var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl;
@ -537,7 +537,7 @@ namespace Avalonia.Skia
var paint = new SKPaint();
Canvas.SaveLayer(paint);
_maskStack.Push(CreatePaint(paint, mask, bounds, true));
_maskStack.Push(CreatePaint(paint, mask, bounds.Size, true));
}
/// <inheritdoc />
@ -593,19 +593,18 @@ namespace Avalonia.Skia
/// <param name="paintWrapper">Paint wrapper.</param>
/// <param name="targetRect">Target bound rect.</param>
/// <param name="gradientBrush">Gradient brush.</param>
private void ConfigureGradientBrush(ref PaintWrapper paintWrapper, Rect targetRect, IGradientBrush gradientBrush)
private void ConfigureGradientBrush(ref PaintWrapper paintWrapper, Size targetSize, IGradientBrush gradientBrush)
{
var tileMode = gradientBrush.SpreadMethod.ToSKShaderTileMode();
var stopColors = gradientBrush.GradientStops.Select(s => s.Color.ToSKColor()).ToArray();
var stopOffsets = gradientBrush.GradientStops.Select(s => (float)s.Offset).ToArray();
var position = targetRect.Position.ToSKPoint();
switch (gradientBrush)
{
case ILinearGradientBrush linearGradient:
{
var start = position + linearGradient.StartPoint.ToPixels(targetRect.Size).ToSKPoint();
var end = position + linearGradient.EndPoint.ToPixels(targetRect.Size).ToSKPoint();
var start = linearGradient.StartPoint.ToPixels(targetSize).ToSKPoint();
var end = linearGradient.EndPoint.ToPixels(targetSize).ToSKPoint();
// would be nice to cache these shaders possibly?
using (var shader =
@ -618,10 +617,10 @@ namespace Avalonia.Skia
}
case IRadialGradientBrush radialGradient:
{
var center = position + radialGradient.Center.ToPixels(targetRect.Size).ToSKPoint();
var radius = (float)(radialGradient.Radius * targetRect.Width);
var center = radialGradient.Center.ToPixels(targetSize).ToSKPoint();
var radius = (float)(radialGradient.Radius * targetSize.Width);
var origin = position + radialGradient.GradientOrigin.ToPixels(targetRect.Size).ToSKPoint();
var origin = radialGradient.GradientOrigin.ToPixels(targetSize).ToSKPoint();
if (origin.Equals(center))
{
@ -666,7 +665,7 @@ namespace Avalonia.Skia
}
case IConicGradientBrush conicGradient:
{
var center = position + conicGradient.Center.ToPixels(targetRect.Size).ToSKPoint();
var center = conicGradient.Center.ToPixels(targetSize).ToSKPoint();
// Skia's default is that angle 0 is from the right hand side of the center point
// but we are matching CSS where the vertical point above the center is 0.
@ -868,10 +867,10 @@ namespace Avalonia.Skia
/// </summary>
/// <param name="paint">The paint to wrap.</param>
/// <param name="brush">Source brush.</param>
/// <param name="targetRect">Target rect.</param>
/// <param name="targetSize">Target size.</param>
/// <param name="disposePaint">Optional dispose of the supplied paint.</param>
/// <returns>Paint wrapper for given brush.</returns>
internal PaintWrapper CreatePaint(SKPaint paint, IBrush brush, Rect targetRect, bool disposePaint = false)
internal PaintWrapper CreatePaint(SKPaint paint, IBrush brush, Size targetSize, bool disposePaint = false)
{
var paintWrapper = new PaintWrapper(paint, disposePaint);
@ -890,7 +889,7 @@ namespace Avalonia.Skia
if (brush is IGradientBrush gradient)
{
ConfigureGradientBrush(ref paintWrapper, targetRect, gradient);
ConfigureGradientBrush(ref paintWrapper, targetSize, gradient);
return paintWrapper;
}
@ -910,7 +909,7 @@ namespace Avalonia.Skia
if (tileBrush != null && tileBrushImage != null)
{
ConfigureTileBrush(ref paintWrapper, targetRect.Size, tileBrush, tileBrushImage);
ConfigureTileBrush(ref paintWrapper, targetSize, tileBrush, tileBrushImage);
}
else
{
@ -925,10 +924,10 @@ namespace Avalonia.Skia
/// </summary>
/// <param name="paint">The paint to wrap.</param>
/// <param name="pen">Source pen.</param>
/// <param name="targetRect">Target rect.</param>
/// <param name="targetSize">Target size.</param>
/// <param name="disposePaint">Optional dispose of the supplied paint.</param>
/// <returns></returns>
private PaintWrapper CreatePaint(SKPaint paint, IPen pen, Rect targetRect, bool disposePaint = false)
private PaintWrapper CreatePaint(SKPaint paint, IPen pen, Size targetSize, bool disposePaint = false)
{
// In Skia 0 thickness means - use hairline rendering
// and for us it means - there is nothing rendered.
@ -937,7 +936,7 @@ namespace Avalonia.Skia
return default;
}
var rv = CreatePaint(paint, pen.Brush, targetRect, disposePaint);
var rv = CreatePaint(paint, pen.Brush, targetSize, disposePaint);
paint.IsStroke = true;
paint.StrokeWidth = (float) pen.Thickness;

4
src/Skia/Avalonia.Skia/FormattedTextImpl.cs

@ -278,9 +278,9 @@ namespace Avalonia.Skia
if (fb != null)
{
//TODO: figure out how to get the brush rect
//TODO: figure out how to get the brush size
currentWrapper = context.CreatePaint(new SKPaint { IsAntialias = true }, fb,
default);
new Size());
}
else
{

36
src/Skia/Avalonia.Skia/GeometryGroupImpl.cs

@ -0,0 +1,36 @@
using System.Collections.Generic;
using Avalonia.Media;
using SkiaSharp;
#nullable enable
namespace Avalonia.Skia
{
/// <summary>
/// A Skia implementation of a <see cref="Avalonia.Media.GeometryGroup"/>.
/// </summary>
internal class GeometryGroupImpl : GeometryImpl
{
public GeometryGroupImpl(FillRule fillRule, IReadOnlyList<Geometry> children)
{
var path = new SKPath
{
FillType = fillRule == FillRule.NonZero ? SKPathFillType.Winding : SKPathFillType.EvenOdd,
};
var count = children.Count;
for (var i = 0; i < count; ++i)
{
if (children[i]?.PlatformImpl is GeometryImpl child)
path.AddPath(child.EffectivePath);
}
EffectivePath = path;
Bounds = path.Bounds.ToAvaloniaRect();
}
public override Rect Bounds { get; }
public override SKPath EffectivePath { get; }
}
}

10
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -62,6 +62,16 @@ namespace Avalonia.Skia
return new StreamGeometryImpl();
}
public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children)
{
return new GeometryGroupImpl(fillRule, children);
}
public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2)
{
return new CombinedGeometryImpl(combineMode, g1, g2);
}
/// <inheritdoc />
public IBitmapImpl LoadBitmap(string fileName)
{

2
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@ -175,6 +175,8 @@ namespace Avalonia.Direct2D1
public IGeometryImpl CreateLineGeometry(Point p1, Point p2) => new LineGeometryImpl(p1, p2);
public IGeometryImpl CreateRectangleGeometry(Rect rect) => new RectangleGeometryImpl(rect);
public IStreamGeometryImpl CreateStreamGeometry() => new StreamGeometryImpl();
public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children) => new GeometryGroupImpl(fillRule, children);
public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2);
/// <inheritdoc />
public IBitmapImpl LoadBitmap(string fileName)

4
src/Windows/Avalonia.Direct2D1/Media/AvaloniaTextRenderer.cs

@ -34,10 +34,10 @@ namespace Avalonia.Direct2D1.Media
{
var wrapper = clientDrawingEffect as BrushWrapper;
// TODO: Work out how to get the rect below rather than passing default.
// TODO: Work out how to get the size below rather than passing new Size().
var brush = (wrapper == null) ?
_foreground :
_context.CreateBrush(wrapper.Brush, default).PlatformBrush;
_context.CreateBrush(wrapper.Brush, new Size()).PlatformBrush;
_renderTarget.DrawGlyphRun(
new RawVector2 { X = baselineOriginX, Y = baselineOriginY },

36
src/Windows/Avalonia.Direct2D1/Media/CombinedGeometryImpl.cs

@ -0,0 +1,36 @@
using SharpDX.Direct2D1;
using AM = Avalonia.Media;
namespace Avalonia.Direct2D1.Media
{
/// <summary>
/// A Direct2D implementation of a <see cref="Avalonia.Media.CombinedGeometry"/>.
/// </summary>
internal class CombinedGeometryImpl : GeometryImpl
{
/// <summary>
/// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
/// </summary>
public CombinedGeometryImpl(
AM.GeometryCombineMode combineMode,
AM.Geometry geometry1,
AM.Geometry geometry2)
: base(CreateGeometry(combineMode, geometry1, geometry2))
{
}
private static Geometry CreateGeometry(
AM.GeometryCombineMode combineMode,
AM.Geometry geometry1,
AM.Geometry geometry2)
{
var g1 = ((GeometryImpl)geometry1.PlatformImpl).Geometry;
var g2 = ((GeometryImpl)geometry2.PlatformImpl).Geometry;
var dest = new PathGeometry(Direct2D1Platform.Direct2D1Factory);
using var sink = dest.Open();
g1.Combine(g2, (CombineMode)combineMode, sink);
sink.Close();
return dest;
}
}
}

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

@ -192,7 +192,7 @@ namespace Avalonia.Direct2D1.Media
{
using (var d2dSource = ((BitmapImpl)source.Item).GetDirect2DBitmap(_deviceContext))
using (var sourceBrush = new BitmapBrush(_deviceContext, d2dSource.Value))
using (var d2dOpacityMask = CreateBrush(opacityMask, opacityMaskRect))
using (var d2dOpacityMask = CreateBrush(opacityMask, opacityMaskRect.Size))
using (var geometry = new SharpDX.Direct2D1.RectangleGeometry(Direct2D1Platform.Direct2D1Factory, destRect.ToDirect2D()))
{
if (d2dOpacityMask.PlatformBrush != null)
@ -217,7 +217,9 @@ namespace Avalonia.Direct2D1.Media
{
if (pen != null)
{
using (var d2dBrush = CreateBrush(pen.Brush, new Rect(p1, p2).Normalize()))
var size = new Rect(p1, p2).Size;
using (var d2dBrush = CreateBrush(pen.Brush, size))
using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext))
{
if (d2dBrush.PlatformBrush != null)
@ -243,7 +245,7 @@ namespace Avalonia.Direct2D1.Media
{
if (brush != null)
{
using (var d2dBrush = CreateBrush(brush, geometry.Bounds))
using (var d2dBrush = CreateBrush(brush, geometry.Bounds.Size))
{
if (d2dBrush.PlatformBrush != null)
{
@ -255,7 +257,7 @@ namespace Avalonia.Direct2D1.Media
if (pen != null)
{
using (var d2dBrush = CreateBrush(pen.Brush, geometry.GetRenderBounds(pen)))
using (var d2dBrush = CreateBrush(pen.Brush, geometry.GetRenderBounds(pen).Size))
using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext))
{
if (d2dBrush.PlatformBrush != null)
@ -280,7 +282,7 @@ namespace Avalonia.Direct2D1.Media
if (brush != null)
{
using (var b = CreateBrush(brush, rect))
using (var b = CreateBrush(brush, rect.Size))
{
if (b.PlatformBrush != null)
{
@ -309,7 +311,7 @@ namespace Avalonia.Direct2D1.Media
if (pen?.Brush != null)
{
using (var wrapper = CreateBrush(pen.Brush, rect))
using (var wrapper = CreateBrush(pen.Brush, rect.Size))
using (var d2dStroke = pen.ToDirect2DStrokeStyle(_deviceContext))
{
if (wrapper.PlatformBrush != null)
@ -347,7 +349,7 @@ namespace Avalonia.Direct2D1.Media
{
var impl = (FormattedTextImpl)text;
using (var brush = CreateBrush(foreground, impl.Bounds))
using (var brush = CreateBrush(foreground, impl.Bounds.Size))
using (var renderer = new AvaloniaTextRenderer(this, _deviceContext, brush.PlatformBrush))
{
if (brush.PlatformBrush != null)
@ -365,7 +367,7 @@ namespace Avalonia.Direct2D1.Media
/// <param name="glyphRun">The glyph run.</param>
public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun)
{
using (var brush = CreateBrush(foreground, new Rect(glyphRun.Size)))
using (var brush = CreateBrush(foreground, glyphRun.Size))
{
var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl;
@ -456,9 +458,9 @@ namespace Avalonia.Direct2D1.Media
/// Creates a Direct2D brush wrapper for a Avalonia brush.
/// </summary>
/// <param name="brush">The avalonia brush.</param>
/// <param name="destinationRect">The brush's target area.</param>
/// <param name="destinationSize">The size of the brush's target area.</param>
/// <returns>The Direct2D brush wrapper.</returns>
public BrushImpl CreateBrush(IBrush brush, Rect destinationRect)
public BrushImpl CreateBrush(IBrush brush, Size destinationSize)
{
var solidColorBrush = brush as ISolidColorBrush;
var linearGradientBrush = brush as ILinearGradientBrush;
@ -473,11 +475,11 @@ namespace Avalonia.Direct2D1.Media
}
else if (linearGradientBrush != null)
{
return new LinearGradientBrushImpl(linearGradientBrush, _deviceContext, destinationRect);
return new LinearGradientBrushImpl(linearGradientBrush, _deviceContext, destinationSize);
}
else if (radialGradientBrush != null)
{
return new RadialGradientBrushImpl(radialGradientBrush, _deviceContext, destinationRect);
return new RadialGradientBrushImpl(radialGradientBrush, _deviceContext, destinationSize);
}
else if (conicGradientBrush != null)
{
@ -490,7 +492,7 @@ namespace Avalonia.Direct2D1.Media
imageBrush,
_deviceContext,
(BitmapImpl)imageBrush.Source.PlatformImpl.Item,
destinationRect.Size);
destinationSize);
}
else if (visualBrush != null)
{
@ -521,7 +523,7 @@ namespace Avalonia.Direct2D1.Media
visualBrush,
_deviceContext,
new D2DBitmapImpl(intermediate.Bitmap),
destinationRect.Size);
destinationSize);
}
}
}
@ -573,7 +575,7 @@ namespace Avalonia.Direct2D1.Media
ContentBounds = PrimitiveExtensions.RectangleInfinite,
MaskTransform = PrimitiveExtensions.Matrix3x2Identity,
Opacity = 1,
OpacityBrush = CreateBrush(mask, bounds).PlatformBrush
OpacityBrush = CreateBrush(mask, bounds.Size).PlatformBrush
};
var layer = _layerPool.Count != 0 ? _layerPool.Pop() : new Layer(_deviceContext);
_deviceContext.PushLayer(ref parameters, layer);

33
src/Windows/Avalonia.Direct2D1/Media/GeometryGroupImpl.cs

@ -0,0 +1,33 @@
using System.Collections.Generic;
using SharpDX.Direct2D1;
using AM = Avalonia.Media;
namespace Avalonia.Direct2D1.Media
{
/// <summary>
/// A Direct2D implementation of a <see cref="Avalonia.Media.GeometryGroup"/>.
/// </summary>
internal class GeometryGroupImpl : GeometryImpl
{
/// <summary>
/// Initializes a new instance of the <see cref="StreamGeometryImpl"/> class.
/// </summary>
public GeometryGroupImpl(AM.FillRule fillRule, IReadOnlyList<AM.Geometry> geometry)
: base(CreateGeometry(fillRule, geometry))
{
}
private static Geometry CreateGeometry(AM.FillRule fillRule, IReadOnlyList<AM.Geometry> children)
{
var count = children.Count;
var c = new Geometry[count];
for (var i = 0; i < count; ++i)
{
c[i] = ((GeometryImpl)children[i].PlatformImpl).Geometry;
}
return new GeometryGroup(Direct2D1Platform.Direct2D1Factory, (FillMode)fillRule, c);
}
}
}

7
src/Windows/Avalonia.Direct2D1/Media/LinearGradientBrushImpl.cs

@ -8,7 +8,7 @@ namespace Avalonia.Direct2D1.Media
public LinearGradientBrushImpl(
ILinearGradientBrush brush,
SharpDX.Direct2D1.RenderTarget target,
Rect destinationRect)
Size destinationSize)
{
if (brush.GradientStops.Count == 0)
{
@ -21,9 +21,8 @@ namespace Avalonia.Direct2D1.Media
Position = (float)s.Offset
}).ToArray();
var position = destinationRect.Position;
var startPoint = position + brush.StartPoint.ToPixels(destinationRect.Size);
var endPoint = position + brush.EndPoint.ToPixels(destinationRect.Size);
var startPoint = brush.StartPoint.ToPixels(destinationSize);
var endPoint = brush.EndPoint.ToPixels(destinationSize);
using (var stops = new SharpDX.Direct2D1.GradientStopCollection(
target,

11
src/Windows/Avalonia.Direct2D1/Media/RadialGradientBrushImpl.cs

@ -8,7 +8,7 @@ namespace Avalonia.Direct2D1.Media
public RadialGradientBrushImpl(
IRadialGradientBrush brush,
SharpDX.Direct2D1.RenderTarget target,
Rect destinationRect)
Size destinationSize)
{
if (brush.GradientStops.Count == 0)
{
@ -21,13 +21,12 @@ namespace Avalonia.Direct2D1.Media
Position = (float)s.Offset
}).ToArray();
var position = destinationRect.Position;
var centerPoint = position + brush.Center.ToPixels(destinationRect.Size);
var gradientOrigin = position + brush.GradientOrigin.ToPixels(destinationRect.Size) - centerPoint;
var centerPoint = brush.Center.ToPixels(destinationSize);
var gradientOrigin = brush.GradientOrigin.ToPixels(destinationSize) - centerPoint;
// Note: Direct2D supports RadiusX and RadiusY but Cairo backend supports only Radius property
var radiusX = brush.Radius * destinationRect.Width;
var radiusY = brush.Radius * destinationRect.Height;
var radiusX = brush.Radius * destinationSize.Width;
var radiusY = brush.Radius * destinationSize.Height;
using (var stops = new SharpDX.Direct2D1.GradientStopCollection(
target,

25
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@ -808,6 +808,31 @@ namespace Avalonia.Win32.Interop
MNC_SELECT = 3
}
public enum SysCommands
{
SC_SIZE = 0xF000,
SC_MOVE = 0xF010,
SC_MINIMIZE = 0xF020,
SC_MAXIMIZE = 0xF030,
SC_NEXTWINDOW = 0xF040,
SC_PREVWINDOW = 0xF050,
SC_CLOSE = 0xF060,
SC_VSCROLL = 0xF070,
SC_HSCROLL = 0xF080,
SC_MOUSEMENU = 0xF090,
SC_KEYMENU = 0xF100,
SC_ARRANGE = 0xF110,
SC_RESTORE = 0xF120,
SC_TASKLIST = 0xF130,
SC_SCREENSAVE = 0xF140,
SC_HOTKEY = 0xF150,
SC_DEFAULT = 0xF160,
SC_MONITORPOWER = 0xF170,
SC_CONTEXTHELP = 0xF180,
SC_SEPARATOR = 0xF00F,
SCF_ISSECURE = 0x00000001,
}
[StructLayout(LayoutKind.Sequential)]
public struct RGBQUAD
{

6
src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs

@ -123,6 +123,12 @@ namespace Avalonia.Win32
break;
}
case WindowsMessage.WM_SYSCOMMAND:
// Disable system handling of Alt/F10 menu keys.
if ((SysCommands)wParam == SysCommands.SC_KEYMENU && HighWord(ToInt32(lParam)) <= 0)
return IntPtr.Zero;
break;
case WindowsMessage.WM_MENUCHAR:
{
// mute the system beep

6
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -263,10 +263,8 @@ namespace Avalonia.Win32
{
ShowWindow(value, true);
}
else
{
_showWindowState = value;
}
_showWindowState = value;
}
}

1
tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj

@ -14,6 +14,7 @@
<Import Project="..\..\build\SharedVersion.props" />
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\..\src\Markup\Avalonia.Markup.Xaml.Loader\Avalonia.Markup.Xaml.Loader.csproj" />
<ProjectReference Include="..\Avalonia.UnitTests\Avalonia.UnitTests.csproj" />
</ItemGroup>
<ItemGroup>

73
tests/Avalonia.Base.UnitTests/Logging/LoggingTests.cs

@ -0,0 +1,73 @@
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Markup.Xaml;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Base.UnitTests.Logging
{
public class LoggingTests
{
[Fact]
public void Control_Should_Not_Log_Binding_Errors_When_Detached_From_Visual_Tree()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Base.UnitTests.Logging;assembly=Avalonia.UnitTests'>
<Panel Name='panel'>
<Rectangle Name='rect' Fill='{Binding $parent[Window].Background}'/>
</Panel>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var calledTimes = 0;
using var logSink = TestLogSink.Start((l, a, s, m, d) =>
{
if (l >= Avalonia.Logging.LogEventLevel.Warning)
{
calledTimes++;
}
});
var panel = window.FindControl<Panel>("panel");
var rect = window.FindControl<Rectangle>("rect");
window.ApplyTemplate();
window.Presenter.ApplyTemplate();
panel.Children.Remove(rect);
Assert.Equal(0, calledTimes);
}
}
[Fact]
public void Control_Should_Log_Binding_Errors_When_No_Ancestor_With_Such_Name()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Base.UnitTests.Logging;assembly=Avalonia.UnitTests'>
<Panel>
<Rectangle Fill='{Binding $parent[Grid].Background}'/>
</Panel>
</Window>";
var calledTimes = 0;
using var logSink = TestLogSink.Start((l, a, s, m, d) =>
{
if (l >= Avalonia.Logging.LogEventLevel.Warning && s is Rectangle)
{
calledTimes++;
}
});
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
window.ApplyTemplate();
window.Presenter.ApplyTemplate();
Assert.Equal(1, calledTimes);
}
}
}
}

10
tests/Avalonia.Benchmarks/NullRenderingPlatform.cs

@ -36,6 +36,16 @@ namespace Avalonia.Benchmarks
return new MockStreamGeometryImpl();
}
public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children)
{
throw new NotImplementedException();
}
public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2)
{
throw new NotImplementedException();
}
public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces)
{
throw new NotImplementedException();

10
tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs

@ -105,6 +105,16 @@ namespace Avalonia.Controls.UnitTests
});
}
[Fact]
public void Custom_FilterMode_Without_ItemFilter_Setting_Throws_Exception()
{
RunTest((control, textbox) =>
{
control.FilterMode = AutoCompleteFilterMode.Custom;
Assert.Throws<Exception>(() => { control.Text = "a"; });
});
}
[Fact]
public void Text_Completion_Via_Text_Property()
{

990
tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs

@ -0,0 +1,990 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.UnitTests;
using Moq;
using Xunit;
namespace Avalonia.Controls.UnitTests
{
public class MaskedTextBoxTests
{
[Fact]
public void Opening_Context_Menu_Does_not_Lose_Selection()
{
using (Start(FocusServices))
{
var target1 = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "1234",
ContextMenu = new TestContextMenu()
};
var target2 = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "5678"
};
var sp = new StackPanel();
sp.Children.Add(target1);
sp.Children.Add(target2);
target1.ApplyTemplate();
target2.ApplyTemplate();
var root = new TestRoot() { Child = sp };
target1.SelectionStart = 0;
target1.SelectionEnd = 3;
target1.Focus();
Assert.False(target2.IsFocused);
Assert.True(target1.IsFocused);
target2.Focus();
Assert.Equal("123", target1.SelectedText);
}
}
[Fact]
public void Opening_Context_Flyout_Does_not_Lose_Selection()
{
using (Start(FocusServices))
{
var target1 = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "1234",
ContextFlyout = new MenuFlyout
{
Items = new List<MenuItem>
{
new MenuItem { Header = "Item 1" },
new MenuItem {Header = "Item 2" },
new MenuItem {Header = "Item 3" }
}
}
};
target1.ApplyTemplate();
var root = new TestRoot() { Child = target1 };
target1.SelectionStart = 0;
target1.SelectionEnd = 3;
target1.Focus();
Assert.True(target1.IsFocused);
target1.ContextFlyout.ShowAt(target1);
Assert.Equal("123", target1.SelectedText);
}
}
[Fact]
public void DefaultBindingMode_Should_Be_TwoWay()
{
Assert.Equal(
BindingMode.TwoWay,
TextBox.TextProperty.GetMetadata(typeof(MaskedTextBox)).DefaultBindingMode);
}
[Fact]
public void CaretIndex_Can_Moved_To_Position_After_The_End_Of_Text_With_Arrow_Key()
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "1234"
};
target.CaretIndex = 3;
RaiseKeyEvent(target, Key.Right, 0);
Assert.Equal(4, target.CaretIndex);
}
}
[Fact]
public void Press_Ctrl_A_Select_All_Text()
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "1234"
};
RaiseKeyEvent(target, Key.A, KeyModifiers.Control);
Assert.Equal(0, target.SelectionStart);
Assert.Equal(4, target.SelectionEnd);
}
}
[Fact]
public void Press_Ctrl_A_Select_All_Null_Text()
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate()
};
RaiseKeyEvent(target, Key.A, KeyModifiers.Control);
Assert.Equal(0, target.SelectionStart);
Assert.Equal(0, target.SelectionEnd);
}
}
[Fact]
public void Press_Ctrl_Z_Will_Not_Modify_Text()
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "1234"
};
RaiseKeyEvent(target, Key.Z, KeyModifiers.Control);
Assert.Equal("1234", target.Text);
}
}
[Fact]
public void Typing_Beginning_With_0_Should_Not_Modify_Text_When_Bound_To_Int()
{
using (Start())
{
var source = new Class1();
var target = new MaskedTextBox
{
DataContext = source,
Template = CreateTemplate(),
};
target.ApplyTemplate();
target.Bind(TextBox.TextProperty, new Binding(nameof(Class1.Foo), BindingMode.TwoWay));
Assert.Equal("0", target.Text);
target.CaretIndex = 1;
target.RaiseEvent(new TextInputEventArgs
{
RoutedEvent = InputElement.TextInputEvent,
Text = "2",
});
Assert.Equal("02", target.Text);
}
}
[Fact]
public void Control_Backspace_Should_Remove_The_Word_Before_The_Caret_If_There_Is_No_Selection()
{
using (Start())
{
MaskedTextBox textBox = new MaskedTextBox
{
Text = "First Second Third Fourth",
CaretIndex = 5
};
// (First| Second Third Fourth)
RaiseKeyEvent(textBox, Key.Back, KeyModifiers.Control);
Assert.Equal(" Second Third Fourth", textBox.Text);
// ( Second |Third Fourth)
textBox.CaretIndex = 8;
RaiseKeyEvent(textBox, Key.Back, KeyModifiers.Control);
Assert.Equal(" Third Fourth", textBox.Text);
// ( Thi|rd Fourth)
textBox.CaretIndex = 4;
RaiseKeyEvent(textBox, Key.Back, KeyModifiers.Control);
Assert.Equal(" rd Fourth", textBox.Text);
// ( rd F[ou]rth)
textBox.SelectionStart = 5;
textBox.SelectionEnd = 7;
RaiseKeyEvent(textBox, Key.Back, KeyModifiers.Control);
Assert.Equal(" rd Frth", textBox.Text);
// ( |rd Frth)
textBox.CaretIndex = 1;
RaiseKeyEvent(textBox, Key.Back, KeyModifiers.Control);
Assert.Equal("rd Frth", textBox.Text);
}
}
[Fact]
public void Control_Delete_Should_Remove_The_Word_After_The_Caret_If_There_Is_No_Selection()
{
using (Start())
{
var textBox = new MaskedTextBox
{
Text = "First Second Third Fourth",
CaretIndex = 19
};
// (First Second Third |Fourth)
RaiseKeyEvent(textBox, Key.Delete, KeyModifiers.Control);
Assert.Equal("First Second Third ", textBox.Text);
// (First Second |Third )
textBox.CaretIndex = 13;
RaiseKeyEvent(textBox, Key.Delete, KeyModifiers.Control);
Assert.Equal("First Second ", textBox.Text);
// (First Sec|ond )
textBox.CaretIndex = 9;
RaiseKeyEvent(textBox, Key.Delete, KeyModifiers.Control);
Assert.Equal("First Sec", textBox.Text);
// (Fi[rs]t Sec )
textBox.SelectionStart = 2;
textBox.SelectionEnd = 4;
RaiseKeyEvent(textBox, Key.Delete, KeyModifiers.Control);
Assert.Equal("Fit Sec", textBox.Text);
// (Fit Sec| )
textBox.Text += " ";
textBox.CaretIndex = 7;
RaiseKeyEvent(textBox, Key.Delete, KeyModifiers.Control);
Assert.Equal("Fit Sec", textBox.Text);
}
}
[Fact]
public void Setting_SelectionStart_To_SelectionEnd_Sets_CaretPosition_To_SelectionStart()
{
using (Start())
{
var textBox = new MaskedTextBox
{
Text = "0123456789"
};
textBox.SelectionStart = 2;
textBox.SelectionEnd = 2;
Assert.Equal(2, textBox.CaretIndex);
}
}
[Fact]
public void Setting_Text_Updates_CaretPosition()
{
using (Start())
{
var target = new MaskedTextBox
{
Text = "Initial Text",
CaretIndex = 11
};
var invoked = false;
target.GetObservable(TextBox.TextProperty).Skip(1).Subscribe(_ =>
{
// Caret index should be set before Text changed notification, as we don't want
// to notify with an invalid CaretIndex.
Assert.Equal(7, target.CaretIndex);
invoked = true;
});
target.Text = "Changed";
Assert.True(invoked);
}
}
[Fact]
public void Press_Enter_Does_Not_Accept_Return()
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
AcceptsReturn = false,
Text = "1234"
};
RaiseKeyEvent(target, Key.Enter, 0);
Assert.Equal("1234", target.Text);
}
}
[Fact]
public void Press_Enter_Add_Default_Newline()
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
AcceptsReturn = true
};
RaiseKeyEvent(target, Key.Enter, 0);
Assert.Equal(Environment.NewLine, target.Text);
}
}
[Theory]
[InlineData("00/00/0000", "12102000", "12/10/2000")]
[InlineData("LLLL", "дбs", "____")]
[InlineData("AA", "Ü1", "__")]
public void AsciiOnly_Should_Not_Accept_Non_Ascii(string mask, string textEventArg, string expected)
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Mask = mask,
AsciiOnly = true
};
RaiseTextEvent(target, textEventArg);
Assert.Equal(expected, target.Text);
}
}
[Fact]
public void Programmatically_Set_Text_Should_Not_Be_Removed_On_Key_Press()
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Mask = "00:00:00.000",
Text = "12:34:56.000"
};
target.CaretIndex = target.Text.Length;
RaiseKeyEvent(target, Key.Back, 0);
Assert.Equal("12:34:56.00_", target.Text);
}
}
[Fact]
public void Invalid_Programmatically_Set_Text_Should_Be_Rejected()
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Mask = "00:00:00.000",
Text = "12:34:560000"
};
Assert.Equal("__:__:__.___", target.Text);
}
}
[Theory]
[InlineData("00/00/0000", "12102000", "**/**/****")]
[InlineData("LLLL", "дбs", "***_")]
[InlineData("AA#00", "S2 33", "**_**")]
public void PasswordChar_Should_Hide_User_Input(string mask, string textEventArg, string expected)
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Mask = mask,
PasswordChar = '*'
};
RaiseTextEvent(target, textEventArg);
Assert.Equal(expected, target.Text);
}
}
[Theory]
[InlineData("00/00/0000", "12102000", "12/10/2000")]
[InlineData("LLLL", "дбs", "дбs_")]
[InlineData("AA#00", "S2 33", "S2_33")]
public void Mask_Should_Work_Correctly(string mask, string textEventArg, string expected)
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Mask = mask
};
RaiseTextEvent(target, textEventArg);
Assert.Equal(expected, target.Text);
}
}
[Fact]
public void Press_Enter_Add_Custom_Newline()
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
AcceptsReturn = true,
NewLine = "Test"
};
RaiseKeyEvent(target, Key.Enter, 0);
Assert.Equal("Test", target.Text);
}
}
[Theory]
[InlineData(new object[] { false, TextWrapping.NoWrap, ScrollBarVisibility.Hidden })]
[InlineData(new object[] { false, TextWrapping.Wrap, ScrollBarVisibility.Disabled })]
[InlineData(new object[] { true, TextWrapping.NoWrap, ScrollBarVisibility.Auto })]
[InlineData(new object[] { true, TextWrapping.Wrap, ScrollBarVisibility.Disabled })]
public void Has_Correct_Horizontal_ScrollBar_Visibility(
bool acceptsReturn,
TextWrapping wrapping,
ScrollBarVisibility expected)
{
using (Start())
{
var target = new MaskedTextBox
{
AcceptsReturn = acceptsReturn,
TextWrapping = wrapping,
};
Assert.Equal(expected, ScrollViewer.GetHorizontalScrollBarVisibility(target));
}
}
[Fact]
public void SelectionEnd_Doesnt_Cause_Exception()
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "0123456789"
};
target.SelectionStart = 0;
target.SelectionEnd = 9;
target.Text = "123";
RaiseTextEvent(target, "456");
Assert.True(true);
}
}
[Fact]
public void SelectionStart_Doesnt_Cause_Exception()
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "0123456789"
};
target.SelectionStart = 8;
target.SelectionEnd = 9;
target.Text = "123";
RaiseTextEvent(target, "456");
Assert.True(true);
}
}
[Fact]
public void SelectionStartEnd_Are_Valid_AterTextChange()
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "0123456789"
};
target.SelectionStart = 8;
target.SelectionEnd = 9;
target.Text = "123";
Assert.True(target.SelectionStart <= "123".Length);
Assert.True(target.SelectionEnd <= "123".Length);
}
}
[Fact]
public void SelectedText_Changes_OnSelectionChange()
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "0123456789"
};
Assert.True(target.SelectedText == "");
target.SelectionStart = 2;
target.SelectionEnd = 4;
Assert.True(target.SelectedText == "23");
}
}
[Fact]
public void SelectedText_EditsText()
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "0123"
};
target.SelectedText = "AA";
Assert.True(target.Text == "AA0123");
target.SelectionStart = 1;
target.SelectionEnd = 3;
target.SelectedText = "BB";
Assert.True(target.Text == "ABB123");
}
}
[Fact]
public void SelectedText_CanClearText()
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "0123"
};
target.SelectionStart = 1;
target.SelectionEnd = 3;
target.SelectedText = "";
Assert.True(target.Text == "03");
}
}
[Fact]
public void SelectedText_NullClearsText()
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "0123"
};
target.SelectionStart = 1;
target.SelectionEnd = 3;
target.SelectedText = null;
Assert.True(target.Text == "03");
}
}
[Fact]
public void CoerceCaretIndex_Doesnt_Cause_Exception_with_malformed_line_ending()
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "0123456789\r"
};
target.CaretIndex = 11;
Assert.True(true);
}
}
[Theory]
[InlineData(Key.Up)]
[InlineData(Key.Down)]
[InlineData(Key.Home)]
[InlineData(Key.End)]
public void Textbox_doesnt_crash_when_Receives_input_and_template_not_applied(Key key)
{
using (Start(FocusServices))
{
var target1 = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "1234",
IsVisible = false
};
var root = new TestRoot { Child = target1 };
target1.Focus();
Assert.True(target1.IsFocused);
RaiseKeyEvent(target1, key, KeyModifiers.None);
}
}
[Fact]
public void TextBox_GotFocus_And_LostFocus_Work_Properly()
{
using (Start(FocusServices))
{
var target1 = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "1234"
};
var target2 = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "5678"
};
var sp = new StackPanel();
sp.Children.Add(target1);
sp.Children.Add(target2);
target1.ApplyTemplate();
target2.ApplyTemplate();
var root = new TestRoot { Child = sp };
var gfcount = 0;
var lfcount = 0;
target1.GotFocus += (s, e) => gfcount++;
target2.LostFocus += (s, e) => lfcount++;
target2.Focus();
Assert.False(target1.IsFocused);
Assert.True(target2.IsFocused);
target1.Focus();
Assert.False(target2.IsFocused);
Assert.True(target1.IsFocused);
Assert.Equal(1, gfcount);
Assert.Equal(1, lfcount);
}
}
[Fact]
public void TextBox_CaretIndex_Persists_When_Focus_Lost()
{
using (Start(FocusServices))
{
var target1 = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "1234"
};
var target2 = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "5678"
};
var sp = new StackPanel();
sp.Children.Add(target1);
sp.Children.Add(target2);
target1.ApplyTemplate();
target2.ApplyTemplate();
var root = new TestRoot { Child = sp };
target2.Focus();
target2.CaretIndex = 2;
Assert.False(target1.IsFocused);
Assert.True(target2.IsFocused);
target1.Focus();
Assert.Equal(2, target2.CaretIndex);
}
}
[Fact]
public void TextBox_Reveal_Password_Reset_When_Lost_Focus()
{
using (Start(FocusServices))
{
var target1 = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "1234",
PasswordChar = '*'
};
var target2 = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "5678"
};
var sp = new StackPanel();
sp.Children.Add(target1);
sp.Children.Add(target2);
target1.ApplyTemplate();
target2.ApplyTemplate();
var root = new TestRoot { Child = sp };
target1.Focus();
target1.RevealPassword = true;
target2.Focus();
Assert.False(target1.RevealPassword);
}
}
[Fact]
public void Setting_Bound_Text_To_Null_Works()
{
using (Start())
{
var source = new Class1 { Bar = "bar" };
var target = new MaskedTextBox { DataContext = source };
target.Bind(TextBox.TextProperty, new Binding("Bar"));
Assert.Equal("bar", target.Text);
source.Bar = null;
Assert.Null(target.Text);
}
}
[Theory]
[InlineData("abc", "d", 3, 0, 0, false, "abc")]
[InlineData("abc", "dd", 4, 3, 3, false, "abcd")]
[InlineData("abc", "ddd", 3, 0, 2, true, "ddc")]
[InlineData("abc", "dddd", 4, 1, 3, true, "addd")]
[InlineData("abc", "ddddd", 5, 3, 3, true, "abcdd")]
public void MaxLength_Works_Properly(
string initalText,
string textInput,
int maxLength,
int selectionStart,
int selectionEnd,
bool fromClipboard,
string expected)
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Text = initalText,
MaxLength = maxLength,
SelectionStart = selectionStart,
SelectionEnd = selectionEnd
};
if (fromClipboard)
{
AvaloniaLocator.CurrentMutable.Bind<IClipboard>().ToSingleton<ClipboardStub>();
var clipboard = AvaloniaLocator.CurrentMutable.GetService<IClipboard>();
clipboard.SetTextAsync(textInput).GetAwaiter().GetResult();
RaiseKeyEvent(target, Key.V, KeyModifiers.Control);
clipboard.ClearAsync().GetAwaiter().GetResult();
}
else
{
RaiseTextEvent(target, textInput);
}
Assert.Equal(expected, target.Text);
}
}
[Theory]
[InlineData(Key.X, KeyModifiers.Control)]
[InlineData(Key.Back, KeyModifiers.None)]
[InlineData(Key.Delete, KeyModifiers.None)]
[InlineData(Key.Tab, KeyModifiers.None)]
[InlineData(Key.Enter, KeyModifiers.None)]
public void Keys_Allow_Undo(Key key, KeyModifiers modifiers)
{
using (Start())
{
var target = new MaskedTextBox
{
Template = CreateTemplate(),
Text = "0123",
AcceptsReturn = true,
AcceptsTab = true
};
target.SelectionStart = 1;
target.SelectionEnd = 3;
AvaloniaLocator.CurrentMutable
.Bind<Input.Platform.IClipboard>().ToSingleton<ClipboardStub>();
RaiseKeyEvent(target, key, modifiers);
RaiseKeyEvent(target, Key.Z, KeyModifiers.Control); // undo
Assert.True(target.Text == "0123");
}
}
private static TestServices FocusServices => TestServices.MockThreadingInterface.With(
focusManager: new FocusManager(),
keyboardDevice: () => new KeyboardDevice(),
keyboardNavigation: new KeyboardNavigationHandler(),
inputManager: new InputManager(),
renderInterface: new MockPlatformRenderInterface(),
fontManagerImpl: new MockFontManagerImpl(),
textShaperImpl: new MockTextShaperImpl(),
standardCursorFactory: Mock.Of<ICursorFactory>());
private static TestServices Services => TestServices.MockThreadingInterface.With(
standardCursorFactory: Mock.Of<ICursorFactory>());
private IControlTemplate CreateTemplate()
{
return new FuncControlTemplate<MaskedTextBox>((control, scope) =>
new TextPresenter
{
Name = "PART_TextPresenter",
[!!TextPresenter.TextProperty] = new Binding
{
Path = "Text",
Mode = BindingMode.TwoWay,
Priority = BindingPriority.TemplatedParent,
RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
},
}.RegisterInNameScope(scope));
}
private void RaiseKeyEvent(MaskedTextBox textBox, Key key, KeyModifiers inputModifiers)
{
textBox.RaiseEvent(new KeyEventArgs
{
RoutedEvent = InputElement.KeyDownEvent,
KeyModifiers = inputModifiers,
Key = key
});
}
private void RaiseTextEvent(MaskedTextBox textBox, string text)
{
textBox.RaiseEvent(new TextInputEventArgs
{
RoutedEvent = InputElement.TextInputEvent,
Text = text
});
}
private static IDisposable Start(TestServices services = null)
{
CultureInfo.CurrentCulture = CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("en-US");
return UnitTestApplication.Start(services ?? Services);
}
private class Class1 : NotifyingBase
{
private int _foo;
private string _bar;
public int Foo
{
get { return _foo; }
set { _foo = value; RaisePropertyChanged(); }
}
public string Bar
{
get { return _bar; }
set { _bar = value; RaisePropertyChanged(); }
}
}
private class ClipboardStub : IClipboard // in order to get tests working that use the clipboard
{
private string _text;
public Task<string> GetTextAsync() => Task.FromResult(_text);
public Task SetTextAsync(string text)
{
_text = text;
return Task.CompletedTask;
}
public Task ClearAsync()
{
_text = null;
return Task.CompletedTask;
}
public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask;
public Task<string[]> GetFormatsAsync() => Task.FromResult(Array.Empty<string>());
public Task<object> GetDataAsync(string format) => Task.FromResult((object)null);
}
private class TestContextMenu : ContextMenu
{
public TestContextMenu()
{
IsOpen = true;
}
}
}
}

89
tests/Avalonia.RenderTests/Media/CombinedGeometryTests.cs

@ -0,0 +1,89 @@
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Media;
using Xunit;
#if AVALONIA_SKIA
namespace Avalonia.Skia.RenderTests
#else
namespace Avalonia.Direct2D1.RenderTests.Media
#endif
{
public class CombinedGeometryTests : TestBase
{
public CombinedGeometryTests()
: base(@"Media\CombinedGeometry")
{
}
[Theory]
[InlineData(Avalonia.Media.GeometryCombineMode.Union)]
[InlineData(Avalonia.Media.GeometryCombineMode.Intersect)]
[InlineData(Avalonia.Media.GeometryCombineMode.Xor)]
[InlineData(Avalonia.Media.GeometryCombineMode.Exclude)]
public async Task GeometryCombineMode(GeometryCombineMode mode)
{
var target = new Border
{
Width = 200,
Height = 200,
Background = Brushes.White,
Child = new Path
{
Data = new CombinedGeometry
{
GeometryCombineMode = mode,
Geometry1 = new RectangleGeometry(new Rect(25, 25, 100, 100)),
Geometry2 = new EllipseGeometry
{
Center = new Point(125, 125),
RadiusX = 50,
RadiusY = 50,
}
},
Fill = Brushes.Blue,
Stroke = Brushes.Red,
StrokeThickness = 1,
}
};
var testName = $"{nameof(GeometryCombineMode)}_{mode}";
await RenderToFile(target, testName);
CompareImages(testName);
}
[Fact]
public async Task Geometry1_Transform()
{
var target = new Border
{
Width = 200,
Height = 200,
Background = Brushes.White,
Child = new Path
{
Data = new CombinedGeometry
{
Geometry1 = new RectangleGeometry(new Rect(25, 25, 100, 100))
{
Transform = new RotateTransform(45, 75, 75)
},
Geometry2 = new EllipseGeometry
{
Center = new Point(125, 125),
RadiusX = 50,
RadiusY = 50,
}
},
Fill = Brushes.Blue,
Stroke = Brushes.Red,
StrokeThickness = 1,
}
};
await RenderToFile(target);
CompareImages();
}
}
}

3
tests/Avalonia.RenderTests/Media/ConicGradientBrushTests.cs

@ -200,7 +200,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media
Child = new DrawnControl(c =>
{
c.DrawRectangle(brush, null, new Rect(0, 0, 100, 100));
c.DrawRectangle(brush, null, new Rect(100, 100, 100, 100));
using (c.PushPreTransform(Matrix.CreateTranslation(100, 100)))
c.DrawRectangle(brush, null, new Rect(0, 0, 100, 100));
}),
};

95
tests/Avalonia.RenderTests/Media/GeometryGroupTests.cs

@ -0,0 +1,95 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Xunit;
#if AVALONIA_SKIA
namespace Avalonia.Skia.RenderTests
#else
namespace Avalonia.Direct2D1.RenderTests.Media
#endif
{
public class GeometryGroupTests : TestBase
{
public GeometryGroupTests()
: base(@"Media\GeometryGroup")
{
}
[Theory]
[InlineData(FillRule.EvenOdd)]
[InlineData(FillRule.NonZero)]
public async Task FillRule_Stroke(FillRule fillRule)
{
var target = new Border
{
Width = 200,
Height = 200,
Background = Brushes.White,
Child = new Path
{
Data = new GeometryGroup
{
FillRule = fillRule,
Children =
{
new RectangleGeometry(new Rect(25, 25, 100, 100)),
new EllipseGeometry
{
Center = new Point(125, 125),
RadiusX = 50,
RadiusY = 50,
},
}
},
Fill = Brushes.Blue,
Stroke = Brushes.Red,
StrokeThickness = 1,
}
};
var testName = $"{nameof(FillRule_Stroke)}_{fillRule}";
await RenderToFile(target, testName);
CompareImages(testName);
}
[Fact]
public async Task Child_Transform()
{
var target = new Border
{
Width = 200,
Height = 200,
Background = Brushes.White,
Child = new Path
{
Data = new GeometryGroup
{
Children =
{
new RectangleGeometry(new Rect(25, 25, 100, 100))
{
Transform = new RotateTransform(45, 75, 75)
},
new EllipseGeometry
{
Center = new Point(125, 125),
RadiusX = 50,
RadiusY = 50,
},
}
},
Fill = Brushes.Blue,
Stroke = Brushes.Red,
StrokeThickness = 1,
}
};
await RenderToFile(target);
CompareImages();
}
}
}

12
tests/Avalonia.RenderTests/Media/LinearGradientBrushTests.cs

@ -81,10 +81,10 @@ namespace Avalonia.Direct2D1.RenderTests.Media
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
GradientStops =
{
new GradientStop { Color = Colors.Red, Offset = 0 },
new GradientStop { Color = Colors.Blue, Offset = 1 }
}
{
new GradientStop { Color = Colors.Red, Offset = 0 },
new GradientStop { Color = Colors.Blue, Offset = 1 }
}
};
Decorator target = new Decorator
@ -94,7 +94,9 @@ namespace Avalonia.Direct2D1.RenderTests.Media
Child = new DrawnControl(c =>
{
c.DrawRectangle(brush, null, new Rect(0, 0, 100, 100));
c.DrawRectangle(brush, null, new Rect(100, 100, 100, 100));
using (c.PushPreTransform(Matrix.CreateTranslation(100, 100)))
c.DrawRectangle(brush, null, new Rect(0, 0, 100, 100));
}),
};

3
tests/Avalonia.RenderTests/Media/RadialGradientBrushTests.cs

@ -185,7 +185,8 @@ namespace Avalonia.Direct2D1.RenderTests.Media
Child = new DrawnControl(c =>
{
c.DrawRectangle(brush, null, new Rect(0, 0, 100, 100));
c.DrawRectangle(brush, null, new Rect(100, 100, 100, 100));
using (c.PushPreTransform(Matrix.CreateTranslation(100, 100)))
c.DrawRectangle(brush, null, new Rect(0, 0, 100, 100));
}),
};

10
tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs

@ -52,6 +52,16 @@ namespace Avalonia.UnitTests
return new MockStreamGeometryImpl();
}
public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children)
{
return Mock.Of<IGeometryImpl>();
}
public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2)
{
return Mock.Of<IGeometryImpl>();
}
public IWriteableBitmapImpl CreateWriteableBitmap(
PixelSize size,
Vector dpi,

26
tests/Avalonia.Visuals.UnitTests/Media/GeometryGroupTests.cs

@ -0,0 +1,26 @@
using Avalonia.Media;
using Xunit;
namespace Avalonia.Visuals.UnitTests.Media
{
public class GeometryGroupTests
{
[Fact]
public void Children_Should_Have_Initial_Collection()
{
var target = new GeometryGroup();
Assert.NotNull(target.Children);
}
[Fact]
public void Children_Can_Be_Set_To_Null()
{
var target = new GeometryGroup();
target.Children = null;
Assert.Null(target.Children);
}
}
}

23
tests/Avalonia.Visuals.UnitTests/Media/PathMarkupParserTests.cs

@ -297,5 +297,28 @@ namespace Avalonia.Visuals.UnitTests.Media
Assert.Equal(new Point(20, 20), figure.StartPoint);
}
}
[Fact]
public void Should_Parse_Flags_Without_Separator()
{
var pathGeometry = new PathGeometry();
using (var context = new PathGeometryContext(pathGeometry))
using (var parser = new PathMarkupParser(context))
{
parser.Parse("a.898.898 0 01.27.188");
var figure = pathGeometry.Figures[0];
var segments = figure.Segments;
Assert.NotNull(segments);
Assert.Equal(1, segments.Count);
var arcSegment = segments[0];
Assert.IsType<ArcSegment>(arcSegment);
}
}
}
}

10
tests/Avalonia.Visuals.UnitTests/VisualTree/MockRenderInterface.cs

@ -37,6 +37,16 @@ namespace Avalonia.Visuals.UnitTests.VisualTree
return new MockStreamGeometry();
}
public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<Geometry> children)
{
throw new NotImplementedException();
}
public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2)
{
throw new NotImplementedException();
}
public IBitmapImpl LoadBitmap(Stream stream)
{
throw new NotImplementedException();

BIN
tests/TestFiles/Direct2D1/Media/CombinedGeometry/Geometry1_Transform.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Exclude.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Intersect.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Union.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
tests/TestFiles/Direct2D1/Media/CombinedGeometry/GeometryCombineMode_Xor.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
tests/TestFiles/Direct2D1/Media/GeometryGroup/Child_Transform.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

BIN
tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_Stroke_EvenOdd.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
tests/TestFiles/Direct2D1/Media/GeometryGroup/FillRule_Stroke_NonZero.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
tests/TestFiles/Skia/Media/CombinedGeometry/Geometry1_Transform.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Exclude.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Intersect.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Union.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
tests/TestFiles/Skia/Media/CombinedGeometry/GeometryCombineMode_Xor.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

BIN
tests/TestFiles/Skia/Media/GeometryGroup/Child_Transform.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
tests/TestFiles/Skia/Media/GeometryGroup/FillRule_Stroke_EvenOdd.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
tests/TestFiles/Skia/Media/GeometryGroup/FillRule_Stroke_NonZero.expected.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Loading…
Cancel
Save