diff --git a/.gitmodules b/.gitmodules index 032bc879cc..6e9f2f7c14 100644 --- a/.gitmodules +++ b/.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 diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 6089a06d4f..4b964448df 100644 --- a/Avalonia.Desktop.slnf +++ b/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", diff --git a/Avalonia.sln b/Avalonia.sln index 9c78c109a5..119b7ab197 100644 --- a/Avalonia.sln +++ b/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} diff --git a/external/Tmds.DBus.SourceGenerator b/external/Tmds.DBus.SourceGenerator new file mode 160000 index 0000000000..3b334c4fbc --- /dev/null +++ b/external/Tmds.DBus.SourceGenerator @@ -0,0 +1 @@ +Subproject commit 3b334c4fbce091fc16a812be134a0ea5d2ed8232 diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index 143db003cd..c92eaf3e0e 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/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 }); diff --git a/src/Android/Avalonia.Android/AvaloniaView.cs b/src/Android/Avalonia.Android/AvaloniaView.cs index 3d3cd2995d..c120f9ee77 100644 --- a/src/Android/Avalonia.Android/AvaloniaView.cs +++ b/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); diff --git a/src/Android/Avalonia.Android/SingleViewLifetime.cs b/src/Android/Avalonia.Android/SingleViewLifetime.cs index f8a2ee2894..2b6b4d3359 100644 --- a/src/Android/Avalonia.Android/SingleViewLifetime.cs +++ b/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); } diff --git a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs b/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs index 69bf8cae5f..daffe0ce77 100644 --- a/src/Avalonia.Base/Input/GestureRecognizers/ScrollGestureRecognizer.cs +++ b/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; } } diff --git a/src/Avalonia.Base/Platform/Storage/FilePickerSaveOptions.cs b/src/Avalonia.Base/Platform/Storage/FilePickerSaveOptions.cs index fa4fccd47a..267ba59c71 100644 --- a/src/Avalonia.Base/Platform/Storage/FilePickerSaveOptions.cs +++ b/src/Avalonia.Base/Platform/Storage/FilePickerSaveOptions.cs @@ -7,11 +7,6 @@ namespace Avalonia.Platform.Storage; /// public class FilePickerSaveOptions : PickerOptions { - /// - /// Gets or sets the file name that the file save picker suggests to the user. - /// - public string? SuggestedFileName { get; set; } - /// /// Gets or sets the default extension to be used to save the file. /// diff --git a/src/Avalonia.Base/Platform/Storage/PickerOptions.cs b/src/Avalonia.Base/Platform/Storage/PickerOptions.cs index 07f99f32c8..4fcc85a07a 100644 --- a/src/Avalonia.Base/Platform/Storage/PickerOptions.cs +++ b/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; /// /// Common options for , and methods. @@ -16,4 +19,9 @@ public class PickerOptions /// or . /// public IStorageFolder? SuggestedStartLocation { get; set; } + + /// + /// Gets or sets the file name that the file picker suggests to the user. + /// + public string? SuggestedFileName { get; set; } } diff --git a/src/Avalonia.Base/Visual.cs b/src/Avalonia.Base/Visual.cs index 9774468e16..85dc082ff6 100644 --- a/src/Avalonia.Base/Visual.cs +++ b/src/Avalonia.Base/Visual.cs @@ -195,23 +195,32 @@ namespace Avalonia /// /// Gets a value indicating whether this control and all its parents are visible. /// - public bool IsEffectivelyVisible + public bool IsEffectivelyVisible { get; private set; } = true; + + /// + /// Updates the property based on the parent's + /// . + /// + /// The effective visibility of the parent control. + 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(); diff --git a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs index 880bbb7340..9a2f98f771 100644 --- a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs +++ b/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(); + } } } diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index cbda1dfbe6..01d67582d5 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/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(); 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 _textRuns; private readonly IReadOnlyList>? _textModifier; diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 431be00f4e..8009492d77 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -124,6 +124,12 @@ namespace Avalonia.Controls public static readonly StyledProperty MaxLinesProperty = AvaloniaProperty.Register(nameof(MaxLines)); + /// + /// Defines the property + /// + public static readonly StyledProperty MinLinesProperty = + AvaloniaProperty.Register(nameof(MinLines)); + /// /// Defines the property /// @@ -519,6 +525,15 @@ namespace Avalonia.Controls set => SetValue(MaxLinesProperty, value); } + /// + /// Gets or sets the minimum number of visible lines to size to. + /// + public int MinLines + { + get => GetValue(MinLinesProperty); + set => SetValue(MinLinesProperty, value); + } + /// /// Gets or sets the spacing between characters /// @@ -913,6 +928,10 @@ namespace Avalonia.Controls { InvalidateMeasure(); } + else if (change.Property == MinLinesProperty) + { + InvalidateMeasure(); + } else if (change.Property == UndoLimitProperty) { OnUndoLimitChanged(change.GetNewValue()); @@ -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(Layoutable.MarginProperty); var padding = visual.GetValue(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; } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 44c997f555..988a265655 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/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(); - 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() diff --git a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs index 1b8e0c18e8..6395529278 100644 --- a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs +++ b/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() .FirstOrDefault(a => a.Key == "AvaloniaUseCompiledBindingsByDefault")?.Value; diff --git a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs index 24074f9c8c..0dfab530ea 100644 --- a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs +++ b/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; diff --git a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj index a77be77c87..4659b79821 100644 --- a/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj +++ b/src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj @@ -13,7 +13,13 @@ - + + diff --git a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs b/src/Avalonia.FreeDesktop/DBusSystemDialog.cs index 1b22393337..7e43aaf943 100644 --- a/src/Avalonia.FreeDesktop/DBusSystemDialog.cs +++ b/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> 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> OpenFolderPickerAsync(FolderPickerOpenOptions options) { + if (_version < 3) + return Array.Empty(); + var parentWindow = $"x11:{_handle.Handle:X}"; var chooserOptions = new Dictionary { { "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(); diff --git a/src/Avalonia.Native/SystemDialogs.cs b/src/Avalonia.Native/SystemDialogs.cs index 9106644dc0..76bf2d3bfa 100644 --- a/src/Avalonia.Native/SystemDialogs.cs +++ b/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); diff --git a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/canvas.ts b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/canvas.ts index 47c501cbb7..800a93a220 100644 --- a/src/Browser/Avalonia.Browser/webapp/modules/avalonia/canvas.ts +++ b/src/Browser/Avalonia.Browser/webapp/modules/avalonia/canvas.ts @@ -206,6 +206,7 @@ export class SizeWatcher { static observer: ResizeObserver; static elements: Map; 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); } }; diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 5d603143de..1d4bae7e6a 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/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 children) => throw new NotImplementedException(); - public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGeometryImpl g1, IGeometryImpl g2) => throw new NotImplementedException(); + + public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList 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 surfaces) => new HeadlessRenderTarget(); public bool IsLost => false; diff --git a/src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderConfiguration.cs b/src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderConfiguration.cs index 90e607ddcc..068b116a45 100644 --- a/src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderConfiguration.cs +++ b/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; } } diff --git a/src/Windows/Avalonia.Win32/Win32StorageProvider.cs b/src/Windows/Avalonia.Win32/Win32StorageProvider.cs index 56ba96fcf6..11ae0d6c0c 100644 --- a/src/Windows/Avalonia.Win32/Win32StorageProvider.cs +++ b/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); diff --git a/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs b/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs index 7752518958..dc7f8c6057 100644 --- a/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs +++ b/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> ParseRuntimeDeps(string text) + { + var runtimeDeps = new List>(); + try + { + var value = JSONParser.FromJson>(text); + if (value?.TryGetValue("targets", out var targetsObj) == true + && targetsObj is Dictionary targets) + { + foreach (var target in targets) + { + if (target.Value is Dictionary libraries) + { + foreach (var library in libraries) + { + if ((library.Value as Dictionary)?.TryGetValue("runtime", out var runtimeObj) == true + && runtimeObj is Dictionary 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; } } diff --git a/src/tools/Avalonia.Designer.HostApp/TinyJson.cs b/src/tools/Avalonia.Designer.HostApp/TinyJson.cs new file mode 100644 index 0000000000..5bd19fa6ea --- /dev/null +++ b/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>() API + // - Classes and structs can be parsed too! + // class Foo { public int Value; } + // "{\"Value\":10}".FromJson() + // - Can parse JSON without type information into Dictionary and List e.g. + // "[1,2,3]".FromJson().GetType() == typeof(List) + // "{\"Value\":10}".FromJson().GetType() == typeof(Dictionary) + // - 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> splitArrayPool; + [ThreadStatic] static StringBuilder stringBuilder; + [ThreadStatic] static Dictionary> fieldInfoCache; + [ThreadStatic] static Dictionary> propertyInfoCache; + + public static T FromJson(this string json) + { + // Initialize, if needed, the ThreadStatic variables + if (propertyInfoCache == null) propertyInfoCache = new Dictionary>(); + if (fieldInfoCache == null) fieldInfoCache = new Dictionary>(); + if (stringBuilder == null) stringBuilder = new StringBuilder(); + if (splitArrayPool == null) splitArrayPool = new Stack>(); + + //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 { :, : } and [ , ] into a list of strings + static List Split(string json) + { + List splitArray = splitArrayPool.Count > 0 ? splitArrayPool.Pop() : new List(); + 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 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 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 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 elems = Split(json); + if (elems.Count % 2 != 0) + return null; + var dict = new Dictionary(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 items = Split(json); + var finalList = new List(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 CreateMemberNameDictionary(T[] members) where T : MemberInfo + { + Dictionary nameToMember = new Dictionary(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 elems = Split(json); + if (elems.Count % 2 != 0) + return instance; + + Dictionary nameToField; + Dictionary 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; + } + } +} diff --git a/tests/Avalonia.Base.UnitTests/VisualTests.cs b/tests/Avalonia.Base.UnitTests/VisualTests.cs index 0150a06715..be0ea41a76 100644 --- a/tests/Avalonia.Base.UnitTests/VisualTests.cs +++ b/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); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs index c0afe0c7ff..8191013dad 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs +++ b/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); + } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 9400d5d9a7..f908f6bff4 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/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(); + Assert.Equal("PART_TextPresenter", textPresenter.Name); + Assert.Equal(new Thickness(0), textPresenter.Margin); // Test assumes no margin on TextPresenter + + var scrollViewer = target.FindDescendantOfType(); + 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(); + 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(); + 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() { diff --git a/tests/Avalonia.Headless.UnitTests/RenderingTests.cs b/tests/Avalonia.Headless.UnitTests/RenderingTests.cs index 591863bcdb..119b17cf2f 100644 --- a/tests/Avalonia.Headless.UnitTests/RenderingTests.cs +++ b/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)]