Browse Source

Merge remote-tracking branch 'origin/master' into xy-focus

pull/13947/head
Max Katz 2 years ago
parent
commit
45b3dbaac4
  1. 3
      .gitmodules
  2. 7
      Avalonia.Desktop.slnf
  3. 7
      Avalonia.sln
  4. 1
      external/Tmds.DBus.SourceGenerator
  5. 2
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  6. 7
      src/Android/Avalonia.Android/AvaloniaView.cs
  7. 2
      src/Android/Avalonia.Android/SingleViewLifetime.cs
  8. 9
      src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs
  9. 5
      src/Avalonia.Base/Platform/Storage/FilePickerSaveOptions.cs
  10. 10
      src/Avalonia.Base/Platform/Storage/PickerOptions.cs
  11. 54
      src/Avalonia.Base/Visual.cs
  12. 7
      src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs
  13. 11
      src/Avalonia.Controls/TextBlock.cs
  14. 55
      src/Avalonia.Controls/TextBox.cs
  15. 44
      src/Avalonia.Controls/Window.cs
  16. 2
      src/Avalonia.DesignerSupport/DesignWindowLoader.cs
  17. 2
      src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs
  18. 8
      src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
  19. 16
      src/Avalonia.FreeDesktop/DBusSystemDialog.cs
  20. 2
      src/Avalonia.Native/SystemDialogs.cs
  21. 15
      src/Browser/Avalonia.Browser/webapp/modules/avalonia/canvas.ts
  22. 11
      src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  23. 2
      src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderConfiguration.cs
  24. 4
      src/Windows/Avalonia.Win32/Win32StorageProvider.cs
  25. 45
      src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs
  26. 384
      src/tools/Avalonia.Designer.HostApp/TinyJson.cs
  27. 108
      tests/Avalonia.Base.UnitTests/VisualTests.cs
  28. 16
      tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
  29. 112
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  30. 68
      tests/Avalonia.Headless.UnitTests/RenderingTests.cs

3
.gitmodules

@ -7,3 +7,6 @@
[submodule "nukebuild/il-repack"]
path = nukebuild/il-repack
url = https://github.com/Gillibald/il-repack
[submodule "external/Tmds.DBus.SourceGenerator"]
path = external/Tmds.DBus.SourceGenerator
url = https://github.com/jmacato/Tmds.DBus.SourceGenerator.git

7
Avalonia.Desktop.slnf

@ -2,6 +2,7 @@
"solution": {
"path": "Avalonia.sln",
"projects": [
"external\\Tmds.DBus.SourceGenerator\\Tmds.DBus.SourceGenerator\\Tmds.DBus.SourceGenerator.csproj",
"packages\\Avalonia\\Avalonia.csproj",
"samples\\AppWithoutLifetime\\AppWithoutLifetime.csproj",
"samples\\ControlCatalog.NetCore\\ControlCatalog.NetCore.csproj",
@ -40,13 +41,13 @@
"src\\Markup\\Avalonia.Markup.Xaml\\Avalonia.Markup.Xaml.csproj",
"src\\Markup\\Avalonia.Markup\\Avalonia.Markup.csproj",
"src\\Skia\\Avalonia.Skia\\Avalonia.Skia.csproj",
"src\\Windows\\Avalonia.Direct2D1\\Avalonia.Direct2D1.csproj",
"src\\Windows\\Avalonia.Win32.Interop\\Avalonia.Win32.Interop.csproj",
"src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj",
"src\\tools\\Avalonia.Analyzers\\Avalonia.Analyzers.csproj",
"src\\tools\\Avalonia.Generators\\Avalonia.Generators.csproj",
"src\\tools\\DevAnalyzers\\DevAnalyzers.csproj",
"src\\tools\\DevGenerators\\DevGenerators.csproj",
"src\\Windows\\Avalonia.Direct2D1\\Avalonia.Direct2D1.csproj",
"src\\Windows\\Avalonia.Win32.Interop\\Avalonia.Win32.Interop.csproj",
"src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj",
"tests\\Avalonia.Base.UnitTests\\Avalonia.Base.UnitTests.csproj",
"tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj",
"tests\\Avalonia.Controls.DataGrid.UnitTests\\Avalonia.Controls.DataGrid.UnitTests.csproj",

7
Avalonia.sln

@ -301,6 +301,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BuildTasks", "BuildTasks",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PInvoke", "tests\TestFiles\BuildTasks\PInvoke\PInvoke.csproj", "{0A948D71-99C5-43E9-BACB-B0BA59EA25B4}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tmds.DBus.SourceGenerator", "external\Tmds.DBus.SourceGenerator\Tmds.DBus.SourceGenerator\Tmds.DBus.SourceGenerator.csproj", "{068247A8-21E8-40D2-83CF-8758410FACAD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -710,6 +712,10 @@ Global
{0A948D71-99C5-43E9-BACB-B0BA59EA25B4}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0A948D71-99C5-43E9-BACB-B0BA59EA25B4}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0A948D71-99C5-43E9-BACB-B0BA59EA25B4}.Release|Any CPU.Build.0 = Release|Any CPU
{068247A8-21E8-40D2-83CF-8758410FACAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{068247A8-21E8-40D2-83CF-8758410FACAD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{068247A8-21E8-40D2-83CF-8758410FACAD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{068247A8-21E8-40D2-83CF-8758410FACAD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -798,6 +804,7 @@ Global
{9D6AEF22-221F-4F4B-B335-A4BA510F002C} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B}
{5BF0C3B8-E595-4940-AB30-2DA206C2F085} = {9D6AEF22-221F-4F4B-B335-A4BA510F002C}
{0A948D71-99C5-43E9-BACB-B0BA59EA25B4} = {5BF0C3B8-E595-4940-AB30-2DA206C2F085}
{068247A8-21E8-40D2-83CF-8758410FACAD} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

1
external/Tmds.DBus.SourceGenerator

@ -0,0 +1 @@
Subproject commit 3b334c4fbce091fc16a812be134a0ea5d2ed8232

2
samples/ControlCatalog/Pages/DialogsPage.xaml.cs

@ -222,6 +222,7 @@ namespace ControlCatalog.Pages
{
Title = "Open file",
FileTypeFilter = GetFileTypes(),
SuggestedFileName = "FileName",
SuggestedStartLocation = lastSelectedDirectory,
AllowMultiple = openMultiple.IsChecked == true
});
@ -264,6 +265,7 @@ namespace ControlCatalog.Pages
{
Title = "Folder file",
SuggestedStartLocation = lastSelectedDirectory,
SuggestedFileName = "FileName",
AllowMultiple = openMultiple.IsChecked == true
});

7
src/Android/Avalonia.Android/AvaloniaView.cs

@ -42,6 +42,13 @@ namespace Avalonia.Android
set { _root.Content = value; }
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
_root?.Dispose();
_root = null;
}
public override bool DispatchKeyEvent(KeyEvent e)
{
return _view.View.DispatchKeyEvent(e);

2
src/Android/Avalonia.Android/SingleViewLifetime.cs

@ -15,7 +15,7 @@ namespace Avalonia.Android
_activity = activity;
if (activity is IAvaloniaActivity activableActivity)
{
{
activableActivity.Activated += (_, args) => Activated?.Invoke(this, args);
activableActivity.Deactivated += (_, args) => Deactivated?.Invoke(this, args);
}

9
src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs

@ -21,6 +21,7 @@ namespace Avalonia.Input.GestureRecognizers
private int _gestureId;
private Point _pointerPressedPoint;
private VelocityTracker? _velocityTracker;
private Visual? _rootTarget;
// Movement per second
private Vector _inertia;
@ -99,7 +100,8 @@ namespace Avalonia.Input.GestureRecognizers
EndGesture();
_tracking = e.Pointer;
_gestureId = ScrollGestureEventArgs.GetNextFreeId();
_trackedRootPoint = _pointerPressedPoint = e.GetPosition((Visual?)Target);
_rootTarget = (Visual?)(Target as Visual)?.VisualRoot;
_trackedRootPoint = _pointerPressedPoint = e.GetPosition(_rootTarget);
}
}
@ -107,7 +109,7 @@ namespace Avalonia.Input.GestureRecognizers
{
if (e.Pointer == _tracking)
{
var rootPoint = e.GetPosition((Visual?)Target);
var rootPoint = e.GetPosition(_rootTarget);
if (!_scrolling)
{
if (CanHorizontallyScroll && Math.Abs(_trackedRootPoint.X - rootPoint.X) > ScrollStartDistance)
@ -134,8 +136,8 @@ namespace Avalonia.Input.GestureRecognizers
_velocityTracker?.AddPosition(TimeSpan.FromMilliseconds(e.Timestamp), _pointerPressedPoint - rootPoint);
_lastMoveTimestamp = e.Timestamp;
_trackedRootPoint = rootPoint;
Target!.RaiseEvent(new ScrollGestureEventArgs(_gestureId, vector));
_trackedRootPoint = rootPoint;
e.Handled = true;
}
}
@ -156,6 +158,7 @@ namespace Avalonia.Input.GestureRecognizers
Target!.RaiseEvent(new ScrollGestureEndedEventArgs(_gestureId));
_gestureId = 0;
_lastMoveTimestamp = null;
_rootTarget = null;
}
}

5
src/Avalonia.Base/Platform/Storage/FilePickerSaveOptions.cs

@ -7,11 +7,6 @@ namespace Avalonia.Platform.Storage;
/// </summary>
public class FilePickerSaveOptions : PickerOptions
{
/// <summary>
/// Gets or sets the file name that the file save picker suggests to the user.
/// </summary>
public string? SuggestedFileName { get; set; }
/// <summary>
/// Gets or sets the default extension to be used to save the file.
/// </summary>

10
src/Avalonia.Base/Platform/Storage/PickerOptions.cs

@ -1,4 +1,7 @@
namespace Avalonia.Platform.Storage;
using System.Collections.Generic;
using Avalonia.Platform.Storage;
namespace Avalonia.Platform.Storage;
/// <summary>
/// Common options for <see cref="IStorageProvider.OpenFolderPickerAsync"/>, <see cref="IStorageProvider.OpenFilePickerAsync"/> and <see cref="IStorageProvider.SaveFilePickerAsync"/> methods.
@ -16,4 +19,9 @@ public class PickerOptions
/// or <see cref="IStorageProvider.TryGetWellKnownFolderAsync"/>.
/// </summary>
public IStorageFolder? SuggestedStartLocation { get; set; }
/// <summary>
/// Gets or sets the file name that the file picker suggests to the user.
/// </summary>
public string? SuggestedFileName { get; set; }
}

54
src/Avalonia.Base/Visual.cs

@ -195,23 +195,32 @@ namespace Avalonia
/// <summary>
/// Gets a value indicating whether this control and all its parents are visible.
/// </summary>
public bool IsEffectivelyVisible
public bool IsEffectivelyVisible { get; private set; } = true;
/// <summary>
/// Updates the <see cref="IsEffectivelyVisible"/> property based on the parent's
/// <see cref="IsEffectivelyVisible"/>.
/// </summary>
/// <param name="parentState">The effective visibility of the parent control.</param>
private void UpdateIsEffectivelyVisible(bool parentState)
{
get
{
Visual? node = this;
var isEffectivelyVisible = parentState && IsVisible;
while (node != null)
{
if (!node.IsVisible)
{
return false;
}
if (IsEffectivelyVisible == isEffectivelyVisible)
return;
node = node.VisualParent;
}
IsEffectivelyVisible = isEffectivelyVisible;
return true;
// PERF-SENSITIVE: This is called on entire hierarchy and using foreach or LINQ
// will cause extra allocations and overhead.
var children = VisualChildren;
// ReSharper disable once ForCanBeConvertedToForeach
for (int i = 0; i < children.Count; ++i)
{
var child = children[i];
child.UpdateIsEffectivelyVisible(isEffectivelyVisible);
}
}
@ -453,7 +462,11 @@ namespace Avalonia
{
base.OnPropertyChanged(change);
if (change.Property == FlowDirectionProperty)
if (change.Property == IsVisibleProperty)
{
UpdateIsEffectivelyVisible(VisualParent?.IsEffectivelyVisible ?? true);
}
else if (change.Property == FlowDirectionProperty)
{
InvalidateMirrorTransform();
@ -463,7 +476,7 @@ namespace Avalonia
}
}
}
protected override void LogicalChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
base.LogicalChildrenCollectionChanged(sender, e);
@ -492,14 +505,16 @@ namespace Avalonia
AttachToCompositor(compositingRenderer.Compositor);
}
InvalidateMirrorTransform();
UpdateIsEffectivelyVisible(_visualParent!.IsEffectivelyVisible);
OnAttachedToVisualTree(e);
AttachedToVisualTree?.Invoke(this, e);
InvalidateVisual();
_visualRoot.Renderer.RecalculateChildren(_visualParent!);
if (ZIndex != 0 && VisualParent is Visual parent)
parent.HasNonUniformZIndexChildren = true;
if (ZIndex != 0 && _visualParent is { })
_visualParent.HasNonUniformZIndexChildren = true;
var visualChildren = VisualChildren;
var visualChildrenCount = visualChildren.Count;
@ -529,6 +544,7 @@ namespace Avalonia
}
DisableTransitions();
UpdateIsEffectivelyVisible(true);
OnDetachedFromVisualTree(e);
DetachFromCompositor();

7
src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs

@ -51,6 +51,11 @@ namespace Avalonia.Controls.Embedding
}
protected override Type StyleKeyOverride => typeof(EmbeddableControlRoot);
public void Dispose() => PlatformImpl?.Dispose();
public void Dispose()
{
PlatformImpl?.Dispose();
LayoutManager?.Dispose();
}
}
}

11
src/Avalonia.Controls/TextBlock.cs

@ -672,9 +672,12 @@ namespace Avalonia.Controls
{
_textLayout?.Dispose();
_textLayout = null;
VisualChildren.Clear();
InvalidateVisual();
_textRuns = null;
InvalidateVisual();
InvalidateMeasure();
}
@ -691,8 +694,6 @@ namespace Avalonia.Controls
if (HasComplexContent)
{
VisualChildren.Clear();
var textRuns = new List<TextRun>();
foreach (var inline in inlines!)
@ -876,9 +877,9 @@ namespace Avalonia.Controls
}
}
#pragma warning disable CA1815 // Equals und Gleichheitsoperator für Werttypen außer Kraft setzen
#pragma warning disable CA1815
protected readonly struct InlinesTextSource : ITextSource
#pragma warning restore CA1815 // Equals und Gleichheitsoperator für Werttypen außer Kraft setzen
#pragma warning restore CA1815
{
private readonly IReadOnlyList<TextRun> _textRuns;
private readonly IReadOnlyList<ValueSpan<TextRunProperties>>? _textModifier;

55
src/Avalonia.Controls/TextBox.cs

@ -124,6 +124,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<int> MaxLinesProperty =
AvaloniaProperty.Register<TextBox, int>(nameof(MaxLines));
/// <summary>
/// Defines the <see cref="MinLines"/> property
/// </summary>
public static readonly StyledProperty<int> MinLinesProperty =
AvaloniaProperty.Register<TextBox, int>(nameof(MinLines));
/// <summary>
/// Defines the <see cref="Text"/> property
/// </summary>
@ -519,6 +525,15 @@ namespace Avalonia.Controls
set => SetValue(MaxLinesProperty, value);
}
/// <summary>
/// Gets or sets the minimum number of visible lines to size to.
/// </summary>
public int MinLines
{
get => GetValue(MinLinesProperty);
set => SetValue(MinLinesProperty, value);
}
/// <summary>
/// Gets or sets the spacing between characters
/// </summary>
@ -913,6 +928,10 @@ namespace Avalonia.Controls
{
InvalidateMeasure();
}
else if (change.Property == MinLinesProperty)
{
InvalidateMeasure();
}
else if (change.Property == UndoLimitProperty)
{
OnUndoLimitChanged(change.GetNewValue<int>());
@ -1836,7 +1855,7 @@ namespace Avalonia.Controls
}
SetCurrentValue(SelectionEndProperty, SelectionEnd + offset);
if (moveCaretPosition)
{
_presenter.MoveCaretToTextPosition(SelectionEnd);
@ -2034,7 +2053,7 @@ namespace Avalonia.Controls
var margin = visual.GetValue<Thickness>(Layoutable.MarginProperty);
var padding = visual.GetValue<Thickness>(Decorator.PaddingProperty);
verticalSpace += margin.Top + padding.Top + padding.Bottom + margin.Bottom;
visual = visual.VisualParent;
@ -2073,8 +2092,8 @@ namespace Avalonia.Controls
var selectionStart = CaretIndex;
MoveHorizontal(-1, true, false, false);
if (SelectionEnd > 0 &&
if (SelectionEnd > 0 &&
selectionStart < text.Length && text[selectionStart] == ' ')
{
SetCurrentValue(SelectionEndProperty, SelectionEnd - 1);
@ -2203,30 +2222,46 @@ namespace Avalonia.Controls
var fontSize = FontSize;
var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
var paragraphProperties = TextLayout.CreateTextParagraphProperties(typeface, fontSize, null, default, default, null, default, LineHeight, default);
var textLayout = new TextLayout(new MaxLinesTextSource(MaxLines), paragraphProperties);
var textLayout = new TextLayout(new LineTextSource(MaxLines), paragraphProperties);
var verticalSpace = GetVerticalSpaceBetweenScrollViewerAndPresenter();
maxHeight = Math.Ceiling(textLayout.Height + verticalSpace);
}
_scrollViewer.SetCurrentValue(MaxHeightProperty, maxHeight);
var minHeight = 0.0;
if (MinLines > 0 && double.IsNaN(Height))
{
var fontSize = FontSize;
var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
var paragraphProperties = TextLayout.CreateTextParagraphProperties(typeface, fontSize, null, default, default, null, default, LineHeight, default);
var textLayout = new TextLayout(new LineTextSource(MinLines), paragraphProperties);
var verticalSpace = GetVerticalSpaceBetweenScrollViewerAndPresenter();
minHeight = Math.Ceiling(textLayout.Height + verticalSpace);
}
_scrollViewer.SetCurrentValue(MinHeightProperty, minHeight);
}
return base.MeasureOverride(availableSize);
}
private class MaxLinesTextSource : ITextSource
private class LineTextSource : ITextSource
{
private readonly int _maxLines;
private readonly int _lines;
public MaxLinesTextSource(int maxLines)
public LineTextSource(int lines)
{
_maxLines = maxLines;
_lines = lines;
}
public TextRun? GetTextRun(int textSourceIndex)
{
if (textSourceIndex >= _maxLines)
if (textSourceIndex >= _lines)
{
return null;
}

44
src/Avalonia.Controls/Window.cs

@ -478,16 +478,11 @@ namespace Avalonia.Controls
child.CloseInternal();
}
if (Owner is Window owner)
{
owner.RemoveChild(this);
}
Owner = null;
PlatformImpl?.Dispose();
_showingAsDialog = false;
Owner = null;
}
private bool ShouldCancelClose(WindowClosingEventArgs args)
@ -554,11 +549,6 @@ namespace Avalonia.Controls
StopRendering();
if (Owner is Window owner)
{
owner.RemoveChild(this);
}
if (_children.Count > 0)
{
foreach (var child in _children.ToArray())
@ -567,10 +557,11 @@ namespace Avalonia.Controls
}
}
Owner = null;
PlatformImpl?.Hide();
IsVisible = false;
_shown = false;
Owner = null;
}
}
@ -689,13 +680,7 @@ namespace Avalonia.Controls
LayoutManager.ExecuteInitialLayoutPass();
if (PlatformImpl != null && owner?.PlatformImpl is not null)
{
PlatformImpl.SetParent(owner.PlatformImpl);
}
Owner = owner;
owner?.AddChild(this, false);
SetWindowStartupLocation(owner);
@ -770,9 +755,7 @@ namespace Avalonia.Controls
var result = new TaskCompletionSource<TResult>();
PlatformImpl?.SetParent(owner.PlatformImpl!);
Owner = owner;
owner.AddChild(this, true);
SetWindowStartupLocation(owner);
@ -974,11 +957,6 @@ namespace Avalonia.Controls
base.HandleClosed();
if (Owner is Window owner)
{
owner.RemoveChild(this);
}
Owner = null;
}
@ -1031,6 +1009,20 @@ namespace Avalonia.Controls
PlatformImpl?.SetSystemDecorations(typedNewValue);
}
if (change.Property == OwnerProperty)
{
var oldParent = change.OldValue as Window;
var newParent = change.NewValue as Window;
oldParent?.RemoveChild(this);
newParent?.AddChild(this, _showingAsDialog);
if (PlatformImpl is IWindowImpl impl)
{
impl.SetParent(_showingAsDialog ? newParent?.PlatformImpl! : (newParent?.PlatformImpl ?? null));
}
}
}
protected override AutomationPeer OnCreateAutomationPeer()

2
src/Avalonia.DesignerSupport/DesignWindowLoader.cs

@ -35,7 +35,7 @@ namespace Avalonia.DesignerSupport
new Uri($"avares://{Path.GetFileNameWithoutExtension(assemblyPath)}{xamlFileProjectPath}");
}
var localAsm = assemblyPath != null ? Assembly.LoadFile(Path.GetFullPath(assemblyPath)) : null;
var localAsm = assemblyPath != null ? Assembly.LoadFrom(Path.GetFullPath(assemblyPath)) : null;
var useCompiledBindings = localAsm?.GetCustomAttributes<AssemblyMetadataAttribute>()
.FirstOrDefault(a => a.Key == "AvaloniaUseCompiledBindingsByDefault")?.Value;

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

@ -172,7 +172,7 @@ namespace Avalonia.DesignerSupport.Remote
var transport = CreateTransport(args);
if (transport is ITransportWithEnforcedMethod enforcedMethod)
args.Method = enforcedMethod.PreviewerMethod;
var asm = Assembly.LoadFile(System.IO.Path.GetFullPath(args.AppPath));
var asm = Assembly.LoadFrom(System.IO.Path.GetFullPath(args.AppPath));
var entryPoint = asm.EntryPoint ?? throw Die($"Assembly {args.AppPath} doesn't have an entry point");
Log($"Initializing application in design mode");
Design.IsDesignMode = true;

8
src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj

@ -13,7 +13,13 @@
<ItemGroup>
<PackageReference Include="Tmds.DBus.Protocol" Version="0.15.0" />
<PackageReference Include="Tmds.DBus.SourceGenerator" Version="0.0.13" PrivateAssets="All" />
<ProjectReference
Include="..\..\external\Tmds.DBus.SourceGenerator\Tmds.DBus.SourceGenerator\Tmds.DBus.SourceGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false"
PrivateAssets="all"
/>
<CompilerVisibleItemMetadata Include="AdditionalFiles" MetadataName="DBusGeneratorMode" />
</ItemGroup>
<ItemGroup>

16
src/Avalonia.FreeDesktop/DBusSystemDialog.cs

@ -20,7 +20,7 @@ namespace Avalonia.FreeDesktop
return null;
var dbusFileChooser = new OrgFreedesktopPortalFileChooser(DBusHelper.Connection, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop");
uint version = 0;
uint version;
try
{
version = await dbusFileChooser.GetVersionPropertyAsync();
@ -50,7 +50,7 @@ namespace Avalonia.FreeDesktop
public override bool CanSave => true;
public override bool CanPickFolder => true;
public override bool CanPickFolder => _version >= 3;
public override async Task<IReadOnlyList<IStorageFile>> OpenFilePickerAsync(FilePickerOpenOptions options)
{
@ -61,7 +61,7 @@ namespace Avalonia.FreeDesktop
if (filters is not null)
chooserOptions.Add("filters", filters);
if (options.SuggestedStartLocation?.TryGetLocalPath() is { } folderPath && _version >= 4)
if (options.SuggestedStartLocation?.TryGetLocalPath() is { } folderPath)
chooserOptions.Add("current_folder", new DBusVariantItem("ay", new DBusByteArrayItem(Encoding.UTF8.GetBytes(folderPath + "\0"))));
chooserOptions.Add("multiple", new DBusVariantItem("b", new DBusBoolItem(options.AllowMultiple)));
@ -119,15 +119,21 @@ namespace Avalonia.FreeDesktop
public override async Task<IReadOnlyList<IStorageFolder>> OpenFolderPickerAsync(FolderPickerOpenOptions options)
{
if (_version < 3)
return Array.Empty<IStorageFolder>();
var parentWindow = $"x11:{_handle.Handle:X}";
var chooserOptions = new Dictionary<string, DBusVariantItem>
{
{ "directory", new DBusVariantItem("b", new DBusBoolItem(true)) },
{ "multiple", new DBusVariantItem("b", new DBusBoolItem(options.AllowMultiple)) }
};
if (options.SuggestedStartLocation?.TryGetLocalPath() is { } folderPath && _version >= 4)
if (options.SuggestedFileName is { } currentName)
chooserOptions.Add("current_name", new DBusVariantItem("s", new DBusStringItem(currentName)));
if (options.SuggestedStartLocation?.TryGetLocalPath() is { } folderPath)
chooserOptions.Add("current_folder", new DBusVariantItem("ay", new DBusByteArrayItem(Encoding.UTF8.GetBytes(folderPath + "\0"))));
var objectPath = await _fileChooser.OpenFileAsync(parentWindow, options.Title ?? string.Empty, chooserOptions);
var request = new OrgFreedesktopPortalRequest(_connection, "org.freedesktop.portal.Desktop", objectPath);
var tsc = new TaskCompletionSource<string[]?>();

2
src/Avalonia.Native/SystemDialogs.cs

@ -41,7 +41,7 @@ namespace Avalonia.Native
options.AllowMultiple.AsComBool(),
options.Title ?? string.Empty,
suggestedDirectory,
string.Empty,
options.SuggestedFileName ?? string.Empty,
fileTypes);
var result = await events.Task.ConfigureAwait(false);

15
src/Browser/Avalonia.Browser/webapp/modules/avalonia/canvas.ts

@ -206,6 +206,7 @@ export class SizeWatcher {
static observer: ResizeObserver;
static elements: Map<string, HTMLElement>;
private static lastMove: number;
private static timeoutHandle?: number;
public static observe(element: HTMLElement, elementId: string | undefined, callback: (width: number, height: number) => void): void {
if (!element || !callback) {
@ -220,6 +221,20 @@ export class SizeWatcher {
if (Date.now() - SizeWatcher.lastMove > 33) {
callback(element.clientWidth, element.clientHeight);
SizeWatcher.lastMove = Date.now();
if (SizeWatcher.timeoutHandle) {
clearTimeout(SizeWatcher.timeoutHandle);
SizeWatcher.timeoutHandle = undefined;
}
} else {
if (SizeWatcher.timeoutHandle) {
clearTimeout(SizeWatcher.timeoutHandle);
}
SizeWatcher.timeoutHandle = setTimeout(() => {
callback(element.clientWidth, element.clientHeight);
SizeWatcher.lastMove = Date.now();
SizeWatcher.timeoutHandle = undefined;
}, 100);
}
};

11
src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using Avalonia.Media;
using Avalonia.Platform;
@ -48,8 +49,14 @@ namespace Avalonia.Headless
}
public IStreamGeometryImpl CreateStreamGeometry() => new HeadlessStreamingGeometryStub();
public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<IGeometryImpl> children) => throw new NotImplementedException();
public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) => throw new NotImplementedException();
public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList<IGeometryImpl> children) =>
new HeadlessGeometryStub(children.Count != 0 ?
children.Select(c => c.Bounds).Aggregate((a, b) => a.Union(b)) :
default);
public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2)
=> new HeadlessGeometryStub(g1.Bounds.Union(g2.Bounds));
public IRenderTarget CreateRenderTarget(IEnumerable<object> surfaces) => new HeadlessRenderTarget();
public bool IsLost => false;

2
src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderConfiguration.cs

@ -66,5 +66,5 @@ public record RuntimeXamlDiagnostic(
int? LineNumber,
int? LinePosition)
{
public string? Document { get; init; }
public string? Document { get; set; }
}

4
src/Windows/Avalonia.Win32/Win32StorageProvider.cs

@ -39,7 +39,7 @@ namespace Avalonia.Win32
return await ShowFilePicker(
true, true,
options.AllowMultiple, false,
options.Title, null, options.SuggestedStartLocation, null, null,
options.Title, options.SuggestedFileName, options.SuggestedStartLocation, null, null,
f => new BclStorageFolder(new DirectoryInfo(f)))
.ConfigureAwait(false);
}
@ -49,7 +49,7 @@ namespace Avalonia.Win32
return await ShowFilePicker(
true, false,
options.AllowMultiple, false,
options.Title, null, options.SuggestedStartLocation,
options.Title, options.SuggestedFileName, options.SuggestedStartLocation,
null, options.FileTypeFilter,
f => new BclStorageFile(new FileInfo(f)))
.ConfigureAwait(false);

45
src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs

@ -2,11 +2,10 @@ using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;
using Avalonia.Markup.Xaml;
using Avalonia.Markup.Xaml.XamlIl;
using TinyJson;
namespace Avalonia.Designer.HostApp;
@ -41,7 +40,7 @@ class DesignXamlLoader : AvaloniaXamlLoader.IRuntimeXamlLoader
/*
We can't use any references in the Avalonia.Designer.HostApp. Including even json.
Ideally we would prefer Microsoft.Extensions.DependencyModel package, but can't use it here.
So, instead we need to fallback to some JSON parsing using pretty easy regex.
So, instead we need to fallback to some JSON parsing with copy-paste tiny json.
Json part example:
"Avalonia.Xaml.Interactions/11.0.0-preview5": {
@ -61,21 +60,53 @@ class DesignXamlLoader : AvaloniaXamlLoader.IRuntimeXamlLoader
No need to handle special cases with .NET Framework and GAC.
*/
var text = new StreamReader(stream).ReadToEnd();
var matches = Regex.Matches( text, """runtime"\s*:\s*{\s*"([^"]+)""");
var deps = ParseRuntimeDeps(text);
foreach (Match match in matches)
foreach (var dependencyRuntimeLibs in deps)
{
if (match.Groups[1] is { Success: true } g)
foreach (var runtimeLib in dependencyRuntimeLibs)
{
var assemblyName = Path.GetFileNameWithoutExtension(g.Value);
var assemblyName = Path.GetFileNameWithoutExtension(runtimeLib);
try
{
_ = Assembly.Load(new AssemblyName(assemblyName));
}
catch
{
}
}
}
}
private static List<IEnumerable<string>> ParseRuntimeDeps(string text)
{
var runtimeDeps = new List<IEnumerable<string>>();
try
{
var value = JSONParser.FromJson<Dictionary<string, object>>(text);
if (value?.TryGetValue("targets", out var targetsObj) == true
&& targetsObj is Dictionary<string, object> targets)
{
foreach (var target in targets)
{
if (target.Value is Dictionary<string, object> libraries)
{
foreach (var library in libraries)
{
if ((library.Value as Dictionary<string, object>)?.TryGetValue("runtime", out var runtimeObj) == true
&& runtimeObj is Dictionary<string, object> runtime)
{
runtimeDeps.Add(runtime.Keys);
}
}
}
}
}
}
catch (Exception ex)
{
Console.WriteLine(".deps.json file parsing failed, it might affect previewer stability.\r\n" + ex);
}
return runtimeDeps;
}
}

384
src/tools/Avalonia.Designer.HostApp/TinyJson.cs

@ -0,0 +1,384 @@
// The MIT License (MIT)
// Copyright (c) 2018 Alex Parker
// Code imported from https://github.com/zanders3/json without any changes
#nullable disable
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text;
namespace TinyJson
{
// Really simple JSON parser in ~300 lines
// - Attempts to parse JSON files with minimal GC allocation
// - Nice and simple "[1,2,3]".FromJson<List<int>>() API
// - Classes and structs can be parsed too!
// class Foo { public int Value; }
// "{\"Value\":10}".FromJson<Foo>()
// - Can parse JSON without type information into Dictionary<string,object> and List<object> e.g.
// "[1,2,3]".FromJson<object>().GetType() == typeof(List<object>)
// "{\"Value\":10}".FromJson<object>().GetType() == typeof(Dictionary<string,object>)
// - No JIT Emit support to support AOT compilation on iOS
// - Attempts are made to NOT throw an exception if the JSON is corrupted or invalid: returns null instead.
// - Only public fields and property setters on classes/structs will be written to
//
// Limitations:
// - No JIT Emit support to parse structures quickly
// - Limited to parsing <2GB JSON files (due to int.MaxValue)
// - Parsing of abstract classes or interfaces is NOT supported and will throw an exception.
[RequiresUnreferencedCode("Json Parsing")]
internal static class JSONParser
{
[ThreadStatic] static Stack<List<string>> splitArrayPool;
[ThreadStatic] static StringBuilder stringBuilder;
[ThreadStatic] static Dictionary<Type, Dictionary<string, FieldInfo>> fieldInfoCache;
[ThreadStatic] static Dictionary<Type, Dictionary<string, PropertyInfo>> propertyInfoCache;
public static T FromJson<T>(this string json)
{
// Initialize, if needed, the ThreadStatic variables
if (propertyInfoCache == null) propertyInfoCache = new Dictionary<Type, Dictionary<string, PropertyInfo>>();
if (fieldInfoCache == null) fieldInfoCache = new Dictionary<Type, Dictionary<string, FieldInfo>>();
if (stringBuilder == null) stringBuilder = new StringBuilder();
if (splitArrayPool == null) splitArrayPool = new Stack<List<string>>();
//Remove all whitespace not within strings to make parsing simpler
stringBuilder.Length = 0;
for (int i = 0; i < json.Length; i++)
{
char c = json[i];
if (c == '"')
{
i = AppendUntilStringEnd(true, i, json);
continue;
}
if (char.IsWhiteSpace(c))
continue;
stringBuilder.Append(c);
}
//Parse the thing!
return (T)ParseValue(typeof(T), stringBuilder.ToString());
}
static int AppendUntilStringEnd(bool appendEscapeCharacter, int startIdx, string json)
{
stringBuilder.Append(json[startIdx]);
for (int i = startIdx + 1; i < json.Length; i++)
{
if (json[i] == '\\')
{
if (appendEscapeCharacter)
stringBuilder.Append(json[i]);
stringBuilder.Append(json[i + 1]);
i++;//Skip next character as it is escaped
}
else if (json[i] == '"')
{
stringBuilder.Append(json[i]);
return i;
}
else
stringBuilder.Append(json[i]);
}
return json.Length - 1;
}
//Splits { <value>:<value>, <value>:<value> } and [ <value>, <value> ] into a list of <value> strings
static List<string> Split(string json)
{
List<string> splitArray = splitArrayPool.Count > 0 ? splitArrayPool.Pop() : new List<string>();
splitArray.Clear();
if (json.Length == 2)
return splitArray;
int parseDepth = 0;
stringBuilder.Length = 0;
for (int i = 1; i < json.Length - 1; i++)
{
switch (json[i])
{
case '[':
case '{':
parseDepth++;
break;
case ']':
case '}':
parseDepth--;
break;
case '"':
i = AppendUntilStringEnd(true, i, json);
continue;
case ',':
case ':':
if (parseDepth == 0)
{
splitArray.Add(stringBuilder.ToString());
stringBuilder.Length = 0;
continue;
}
break;
}
stringBuilder.Append(json[i]);
}
splitArray.Add(stringBuilder.ToString());
return splitArray;
}
internal static object ParseValue(Type type, string json)
{
if (type == typeof(string))
{
if (json.Length <= 2)
return string.Empty;
StringBuilder parseStringBuilder = new StringBuilder(json.Length);
for (int i = 1; i < json.Length - 1; ++i)
{
if (json[i] == '\\' && i + 1 < json.Length - 1)
{
int j = "\"\\nrtbf/".IndexOf(json[i + 1]);
if (j >= 0)
{
parseStringBuilder.Append("\"\\\n\r\t\b\f/"[j]);
++i;
continue;
}
if (json[i + 1] == 'u' && i + 5 < json.Length - 1)
{
UInt32 c = 0;
if (UInt32.TryParse(json.Substring(i + 2, 4), System.Globalization.NumberStyles.AllowHexSpecifier, null, out c))
{
parseStringBuilder.Append((char)c);
i += 5;
continue;
}
}
}
parseStringBuilder.Append(json[i]);
}
return parseStringBuilder.ToString();
}
if (type.IsPrimitive)
{
var result = Convert.ChangeType(json, type, System.Globalization.CultureInfo.InvariantCulture);
return result;
}
if (type == typeof(decimal))
{
decimal result;
decimal.TryParse(json, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out result);
return result;
}
if (type == typeof(DateTime))
{
DateTime result;
DateTime.TryParse(json.Replace("\"",""), System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.None, out result);
return result;
}
if (json == "null")
{
return null;
}
if (type.IsEnum)
{
if (json[0] == '"')
json = json.Substring(1, json.Length - 2);
try
{
return Enum.Parse(type, json, false);
}
catch
{
return 0;
}
}
if (type.IsArray)
{
Type arrayType = type.GetElementType();
if (json[0] != '[' || json[json.Length - 1] != ']')
return null;
List<string> elems = Split(json);
Array newArray = Array.CreateInstance(arrayType, elems.Count);
for (int i = 0; i < elems.Count; i++)
newArray.SetValue(ParseValue(arrayType, elems[i]), i);
splitArrayPool.Push(elems);
return newArray;
}
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))
{
Type listType = type.GetGenericArguments()[0];
if (json[0] != '[' || json[json.Length - 1] != ']')
return null;
List<string> elems = Split(json);
var list = (IList)type.GetConstructor(new Type[] { typeof(int) }).Invoke(new object[] { elems.Count });
for (int i = 0; i < elems.Count; i++)
list.Add(ParseValue(listType, elems[i]));
splitArrayPool.Push(elems);
return list;
}
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
{
Type keyType, valueType;
{
Type[] args = type.GetGenericArguments();
keyType = args[0];
valueType = args[1];
}
//Refuse to parse dictionary keys that aren't of type string
if (keyType != typeof(string))
return null;
//Must be a valid dictionary element
if (json[0] != '{' || json[json.Length - 1] != '}')
return null;
//The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON
List<string> elems = Split(json);
if (elems.Count % 2 != 0)
return null;
var dictionary = (IDictionary)type.GetConstructor(new Type[] { typeof(int) }).Invoke(new object[] { elems.Count / 2 });
for (int i = 0; i < elems.Count; i += 2)
{
if (elems[i].Length <= 2)
continue;
string keyValue = elems[i].Substring(1, elems[i].Length - 2);
object val = ParseValue(valueType, elems[i + 1]);
dictionary[keyValue] = val;
}
return dictionary;
}
if (type == typeof(object))
{
return ParseAnonymousValue(json);
}
if (json[0] == '{' && json[json.Length - 1] == '}')
{
return ParseObject(type, json);
}
return null;
}
static object ParseAnonymousValue(string json)
{
if (json.Length == 0)
return null;
if (json[0] == '{' && json[json.Length - 1] == '}')
{
List<string> elems = Split(json);
if (elems.Count % 2 != 0)
return null;
var dict = new Dictionary<string, object>(elems.Count / 2);
for (int i = 0; i < elems.Count; i += 2)
dict[elems[i].Substring(1, elems[i].Length - 2)] = ParseAnonymousValue(elems[i + 1]);
return dict;
}
if (json[0] == '[' && json[json.Length - 1] == ']')
{
List<string> items = Split(json);
var finalList = new List<object>(items.Count);
for (int i = 0; i < items.Count; i++)
finalList.Add(ParseAnonymousValue(items[i]));
return finalList;
}
if (json[0] == '"' && json[json.Length - 1] == '"')
{
string str = json.Substring(1, json.Length - 2);
return str.Replace("\\", string.Empty);
}
if (char.IsDigit(json[0]) || json[0] == '-')
{
if (json.Contains("."))
{
double result;
double.TryParse(json, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out result);
return result;
}
else
{
int result;
int.TryParse(json, out result);
return result;
}
}
if (json == "true")
return true;
if (json == "false")
return false;
// handles json == "null" as well as invalid JSON
return null;
}
static Dictionary<string, T> CreateMemberNameDictionary<T>(T[] members) where T : MemberInfo
{
Dictionary<string, T> nameToMember = new Dictionary<string, T>(StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < members.Length; i++)
{
T member = members[i];
if (member.IsDefined(typeof(IgnoreDataMemberAttribute), true))
continue;
string name = member.Name;
if (member.IsDefined(typeof(DataMemberAttribute), true))
{
DataMemberAttribute dataMemberAttribute = (DataMemberAttribute)Attribute.GetCustomAttribute(member, typeof(DataMemberAttribute), true);
if (!string.IsNullOrEmpty(dataMemberAttribute.Name))
name = dataMemberAttribute.Name;
}
nameToMember.Add(name, member);
}
return nameToMember;
}
static object ParseObject(Type type, string json)
{
object instance = FormatterServices.GetUninitializedObject(type);
//The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON
List<string> elems = Split(json);
if (elems.Count % 2 != 0)
return instance;
Dictionary<string, FieldInfo> nameToField;
Dictionary<string, PropertyInfo> nameToProperty;
if (!fieldInfoCache.TryGetValue(type, out nameToField))
{
nameToField = CreateMemberNameDictionary(type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy));
fieldInfoCache.Add(type, nameToField);
}
if (!propertyInfoCache.TryGetValue(type, out nameToProperty))
{
nameToProperty = CreateMemberNameDictionary(type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy));
propertyInfoCache.Add(type, nameToProperty);
}
for (int i = 0; i < elems.Count; i += 2)
{
if (elems[i].Length <= 2)
continue;
string key = elems[i].Substring(1, elems[i].Length - 2);
string value = elems[i + 1];
FieldInfo fieldInfo;
PropertyInfo propertyInfo;
if (nameToField.TryGetValue(key, out fieldInfo))
fieldInfo.SetValue(instance, ParseValue(fieldInfo.FieldType, value));
else if (nameToProperty.TryGetValue(key, out propertyInfo))
propertyInfo.SetValue(instance, ParseValue(propertyInfo.PropertyType, value), null);
}
return instance;
}
}
}

108
tests/Avalonia.Base.UnitTests/VisualTests.cs

@ -349,5 +349,113 @@ namespace Avalonia.Base.UnitTests
renderer.Verify(x => x.RecalculateChildren(stackPanel));
}
[Theory]
[InlineData(new[] { 1, 2, 3 }, true, true, true, true, true, true)]
[InlineData(new[] { 3, 2, 1 }, true, true, true, true, true, true)]
[InlineData(new[] { 1 }, false, true, true, false, false, false)]
[InlineData(new[] { 2 }, true, false, true, true, false, false)]
[InlineData(new[] { 3 }, true, true, false, true, true, false)]
[InlineData(new[] { 3, 1}, true, true, false, true, true, false)]
[InlineData(new[] { 2, 3, 1 }, true, false, true, true, false, false, true)]
[InlineData(new[] { 3, 1, 2 }, true, true, false, true, true, false, true)]
[InlineData(new[] { 3, 2, 1 }, true, true, false, true, true, false, true)]
public void IsEffectivelyVisible_Propagates_To_Visual_Children(int[] assignOrder, bool rootV, bool child1V,
bool child2V, bool rootExpected, bool child1Expected, bool child2Expected, bool initialSetToFalse = false)
{
using var app = UnitTestApplication.Start();
var child2 = new Decorator();
var child1 = new Decorator { Child = child2 };
var root = new TestRoot { Child = child1 };
Assert.True(child2.IsEffectivelyVisible);
if (initialSetToFalse)
{
root.IsVisible = false;
child1.IsVisible = false;
child2.IsVisible = false;
}
foreach (var order in assignOrder)
{
switch (order)
{
case 1:
root.IsVisible = rootV;
break;
case 2:
child1.IsVisible = child1V;
break;
case 3:
child2.IsVisible = child2V;
break;
}
}
Assert.Equal(rootExpected, root.IsEffectivelyVisible);
Assert.Equal(child1Expected, child1.IsEffectivelyVisible);
Assert.Equal(child2Expected, child2.IsEffectivelyVisible);
}
[Fact]
public void Added_Child_Has_Correct_IsEffectivelyVisible()
{
using var app = UnitTestApplication.Start();
var root = new TestRoot { IsVisible = false };
var child = new Decorator();
root.Child = child;
Assert.False(child.IsEffectivelyVisible);
}
[Fact]
public void Added_Grandchild_Has_Correct_IsEffectivelyVisible()
{
using var app = UnitTestApplication.Start();
var child = new Decorator();
var grandchild = new Decorator();
var root = new TestRoot
{
IsVisible = false,
Child = child
};
child.Child = grandchild;
Assert.False(grandchild.IsEffectivelyVisible);
}
[Fact]
public void Removing_Child_Resets_IsEffectivelyVisible()
{
using var app = UnitTestApplication.Start();
var child = new Decorator();
var root = new TestRoot { Child = child, IsVisible = false };
Assert.False(child.IsEffectivelyVisible);
root.Child = null;
Assert.True(child.IsEffectivelyVisible);
}
[Fact]
public void Removing_Child_Resets_IsEffectivelyVisible_Of_Grandchild()
{
using var app = UnitTestApplication.Start();
var grandchild = new Decorator();
var child = new Decorator { Child = grandchild };
var root = new TestRoot { Child = child, IsVisible = false };
Assert.False(child.IsEffectivelyVisible);
Assert.False(grandchild.IsEffectivelyVisible);
root.Child = null;
Assert.True(child.IsEffectivelyVisible);
Assert.True(grandchild.IsEffectivelyVisible);
}
}
}

16
tests/Avalonia.Controls.UnitTests/TextBlockTests.cs

@ -224,5 +224,21 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(underline, target.Inlines[0].TextDecorations);
}
}
[Fact]
public void TextBlock_TextLines_Should_Be_Empty()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var textblock = new TextBlock();
textblock.Inlines?.Add(new Run("123"));
textblock.Measure(new Size(200, 200));
int count = textblock.TextLayout.TextLines[0].TextRuns.Count;
textblock.Inlines?.Clear();
textblock.Measure(new Size(200, 200));
int count1 = textblock.TextLayout.TextLines[0].TextRuns.Count;
Assert.NotEqual(count, count1);
}
}
}
}

112
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@ -1066,6 +1066,118 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact]
public void Should_Fullfill_MinLines_Contraint()
{
using (UnitTestApplication.Start(Services))
{
var target = new TextBox
{
Template = CreateTemplate(),
Text = "ABC \n DEF \n GHI",
MinLines = 3,
AcceptsReturn = true
};
var impl = CreateMockTopLevelImpl();
var topLevel = new TestTopLevel(impl.Object)
{
Template = CreateTopLevelTemplate()
};
topLevel.Content = target;
topLevel.ApplyTemplate();
topLevel.LayoutManager.ExecuteInitialLayoutPass();
target.ApplyTemplate();
target.Measure(Size.Infinity);
var initialHeight = target.DesiredSize.Height;
target.Text = "";
target.InvalidateMeasure();
target.Measure(Size.Infinity);
Assert.Equal(initialHeight, target.DesiredSize.Height);
}
}
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
public void MinLines_Sets_ScrollViewer_MinHeight(int minLines)
{
using (UnitTestApplication.Start(Services))
{
var target = new TextBox
{
Template = CreateTemplate(),
MinLines = minLines,
// Define explicit whole number line height for predictable calculations
LineHeight = 20
};
var impl = CreateMockTopLevelImpl();
var topLevel = new TestTopLevel(impl.Object)
{
Template = CreateTopLevelTemplate(),
Content = target
};
topLevel.ApplyTemplate();
topLevel.LayoutManager.ExecuteInitialLayoutPass();
var textPresenter = target.FindDescendantOfType<TextPresenter>();
Assert.Equal("PART_TextPresenter", textPresenter.Name);
Assert.Equal(new Thickness(0), textPresenter.Margin); // Test assumes no margin on TextPresenter
var scrollViewer = target.FindDescendantOfType<ScrollViewer>();
Assert.Equal("PART_ScrollViewer", scrollViewer.Name);
Assert.Equal(minLines * target.LineHeight, scrollViewer.MinHeight);
}
}
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
public void MinLines_Sets_ScrollViewer_MinHeight_With_TextPresenter_Margin(int minLines)
{
using (UnitTestApplication.Start(Services))
{
var target = new TextBox
{
Template = CreateTemplate(),
MinLines = minLines,
// Define explicit whole number line height for predictable calculations
LineHeight = 20
};
var impl = CreateMockTopLevelImpl();
var topLevel = new TestTopLevel(impl.Object)
{
Template = CreateTopLevelTemplate(),
Content = target
};
topLevel.ApplyTemplate();
topLevel.LayoutManager.ExecuteInitialLayoutPass();
var textPresenter = target.FindDescendantOfType<TextPresenter>();
Assert.Equal("PART_TextPresenter", textPresenter.Name);
var textPresenterMargin = new Thickness(horizontal: 0, vertical: 3);
textPresenter.Margin = textPresenterMargin;
target.InvalidateMeasure();
target.Measure(Size.Infinity);
var scrollViewer = target.FindDescendantOfType<ScrollViewer>();
Assert.Equal("PART_ScrollViewer", scrollViewer.Name);
Assert.Equal((minLines * target.LineHeight) + textPresenterMargin.Top + textPresenterMargin.Bottom, scrollViewer.MinHeight);
}
}
[Fact]
public void CanUndo_CanRedo_Is_False_When_Initialized()
{

68
tests/Avalonia.Headless.UnitTests/RenderingTests.cs

@ -1,5 +1,6 @@
using System.Collections.ObjectModel;
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Threading;
@ -36,6 +37,73 @@ public class RenderingTests
Assert.NotNull(frame);
}
#if NUNIT
[AvaloniaTest, Timeout(10000)]
#elif XUNIT
[AvaloniaFact(Timeout = 10000)]
#endif
public void Should_Not_Crash_On_GeometryGroup()
{
var window = new Window
{
Content = new ContentControl
{
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
Padding = new Thickness(4),
Content = new PathIcon
{
Data = new GeometryGroup()
{
Children = new GeometryCollection(new []
{
new RectangleGeometry(new Rect(0, 0, 50, 50)),
new RectangleGeometry(new Rect(50, 50, 100, 100))
})
}
}
},
SizeToContent = SizeToContent.WidthAndHeight
};
window.Show();
var frame = window.CaptureRenderedFrame();
Assert.NotNull(frame);
}
#if NUNIT
[AvaloniaTest, Timeout(10000)]
#elif XUNIT
[AvaloniaFact(Timeout = 10000)]
#endif
public void Should_Not_Crash_On_CombinedGeometry()
{
var window = new Window
{
Content = new ContentControl
{
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
Padding = new Thickness(4),
Content = new PathIcon
{
Data = new CombinedGeometry(GeometryCombineMode.Union,
new RectangleGeometry(new Rect(0, 0, 50, 50)),
new RectangleGeometry(new Rect(50, 50, 100, 100)))
}
},
SizeToContent = SizeToContent.WidthAndHeight
};
window.Show();
var frame = window.CaptureRenderedFrame();
Assert.NotNull(frame);
}
#if NUNIT
[AvaloniaTest, Timeout(10000)]

Loading…
Cancel
Save