diff --git a/.gitmodules b/.gitmodules index 9dbc50ef61..2d11fdfa9e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,5 +2,5 @@ path = nukebuild/Numerge url = https://github.com/kekekeks/Numerge.git [submodule "src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github"] - path = src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github + path = src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github url = https://github.com/kekekeks/XamlX.git diff --git a/Avalonia.sln b/Avalonia.sln index 4ab647a25e..ccaa41f552 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -123,10 +123,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.NetCore", "s EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1-27F5-4255-9AFC-04ABFD11683A}" ProjectSection(SolutionItems) = preProject + build\AndroidWorkarounds.props = build\AndroidWorkarounds.props build\Base.props = build\Base.props build\Binding.props = build\Binding.props - build\BuildTargets.targets = build\BuildTargets.targets + build\CoreLibraries.props = build\CoreLibraries.props + build\EmbedXaml.props = build\EmbedXaml.props build\HarfBuzzSharp.props = build\HarfBuzzSharp.props + build\iOSWorkarounds.props = build\iOSWorkarounds.props build\JetBrains.Annotations.props = build\JetBrains.Annotations.props build\JetBrains.dotMemoryUnit.props = build\JetBrains.dotMemoryUnit.props build\Magick.NET-Q16-AnyCPU.props = build\Magick.NET-Q16-AnyCPU.props @@ -136,17 +139,24 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\NetCore.props = build\NetCore.props build\NetFX.props = build\NetFX.props build\ReactiveUI.props = build\ReactiveUI.props + build\ReferenceCoreLibraries.props = build\ReferenceCoreLibraries.props build\Rx.props = build\Rx.props build\SampleApp.props = build\SampleApp.props + build\SharedVersion.props = build\SharedVersion.props build\SharpDX.props = build\SharpDX.props build\SkiaSharp.props = build\SkiaSharp.props + build\SourceLink.props = build\SourceLink.props + build\System.Drawing.Common.props = build\System.Drawing.Common.props build\System.Memory.props = build\System.Memory.props + build\UnitTests.NetFX.props = build\UnitTests.NetFX.props build\XUnit.props = build\XUnit.props EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Targets", "Targets", "{4D6FAF79-58B4-482F-9122-0668C346364C}" ProjectSection(SolutionItems) = preProject build\UnitTests.NetCore.targets = build\UnitTests.NetCore.targets + build\BuildTargets.targets = build\BuildTargets.targets + build\LegacyProject.targets = build\LegacyProject.targets EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Linux", "Linux", "{86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}" @@ -211,6 +221,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless", "src\Av EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.Vnc", "src\Avalonia.Headless.Vnc\Avalonia.Headless.Vnc.csproj", "{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Markup.Xaml.Loader", "src\Markup\Avalonia.Markup.Xaml.Loader\Avalonia.Markup.Xaml.Loader.csproj", "{909A8CBD-7D0E-42FD-B841-022AD8925820}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 @@ -1998,6 +2010,30 @@ Global {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|iPhone.Build.0 = Release|Any CPU {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.AppStore|iPhone.Build.0 = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Debug|Any CPU.Build.0 = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Debug|iPhone.Build.0 = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|Any CPU.ActiveCfg = Release|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|Any CPU.Build.0 = Release|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhone.ActiveCfg = Release|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhone.Build.0 = Release|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2056,6 +2092,7 @@ Global {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {351337F5-D66F-461B-A957-4EF60BDB4BA6} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {3C84E04B-36CF-4D0D-B965-C26DD649D1F3} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} + {909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/Avalonia.v3.ncrunchsolution b/Avalonia.v3.ncrunchsolution index a2208a9a91..bef7e45524 100644 --- a/Avalonia.v3.ncrunchsolution +++ b/Avalonia.v3.ncrunchsolution @@ -3,6 +3,7 @@ tests\TestFiles\**.* src\Avalonia.Build.Tasks\bin\Debug\netstandard2.0\Avalonia.Build.Tasks.dll + src\Avalonia.Build.Tasks\bin\Debug\netstandard2.0\Mono.Cecil.dll True .ncrunch diff --git a/Directory.Build.props b/Directory.Build.props index 1f26df9bbc..b41f8c488e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,6 @@ $(MSBuildThisFileDirectory)build-intermediate/nuget + $(MSBuildThisFileDirectory)\src\tools\Avalonia.Designer.HostApp\bin\$(Configuration)\netcoreapp2.0\Avalonia.Designer.HostApp.dll diff --git a/dirs.proj b/dirs.proj index 4b3b1183f0..26c8f54b23 100644 --- a/dirs.proj +++ b/dirs.proj @@ -6,7 +6,7 @@ - + diff --git a/native/Avalonia.Native/src/OSX/Screens.mm b/native/Avalonia.Native/src/OSX/Screens.mm index 278daf9a18..455cfa2e41 100644 --- a/native/Avalonia.Native/src/OSX/Screens.mm +++ b/native/Avalonia.Native/src/OSX/Screens.mm @@ -4,6 +4,14 @@ class Screens : public ComSingleObject { public: FORWARD_IUNKNOWN() + + private: + CGFloat PrimaryDisplayHeight() + { + return NSMaxY([[[NSScreen screens] firstObject] frame]); + } + +public: virtual HRESULT GetScreenCount (int* ret) override { @autoreleasepool @@ -25,15 +33,15 @@ class Screens : public ComSingleObject auto screen = [[NSScreen screens] objectAtIndex:index]; - ret->Bounds.X = [screen frame].origin.x; - ret->Bounds.Y = [screen frame].origin.y; ret->Bounds.Height = [screen frame].size.height; ret->Bounds.Width = [screen frame].size.width; + ret->Bounds.X = [screen frame].origin.x; + ret->Bounds.Y = PrimaryDisplayHeight() - [screen frame].origin.y - ret->Bounds.Height; - ret->WorkingArea.X = [screen visibleFrame].origin.x; - ret->WorkingArea.Y = [screen visibleFrame].origin.y; ret->WorkingArea.Height = [screen visibleFrame].size.height; ret->WorkingArea.Width = [screen visibleFrame].size.width; + ret->WorkingArea.X = [screen visibleFrame].origin.x; + ret->WorkingArea.Y = ret->Bounds.Height - [screen visibleFrame].origin.y - ret->WorkingArea.Height; ret->PixelDensity = [screen backingScaleFactor]; diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 872269bb26..2d0ffbe4f0 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -1291,10 +1291,15 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent _parent->UpdateCursor(); auto fsize = [self convertSizeToBacking: [self frame].size]; - _lastPixelSize.Width = (int)fsize.width; - _lastPixelSize.Height = (int)fsize.height; - [self updateRenderTarget]; - _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}); + + if(_lastPixelSize.Width != (int)fsize.width || _lastPixelSize.Height != (int)fsize.height) + { + _lastPixelSize.Width = (int)fsize.width; + _lastPixelSize.Height = (int)fsize.height; + [self updateRenderTarget]; + + _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}); + } } - (void)updateLayer diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 0baa6589c6..3f1ec289d1 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -29,11 +29,19 @@ namespace ControlCatalog { new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) { - Source = new Uri("resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default") + Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/Base.xaml") }, new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) { - Source = new Uri("resm:Avalonia.Themes.Default.DefaultTheme.xaml?assembly=Avalonia.Themes.Default") + Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/BaseLight.xaml") + }, + new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) + { + Source = new Uri("avares://Avalonia.Themes.Default/Accents/BaseLight.xaml") + }, + new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) + { + Source = new Uri("avares://Avalonia.Themes.Default/DefaultTheme.xaml") }, }; @@ -41,11 +49,19 @@ namespace ControlCatalog { new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) { - Source = new Uri("resm:Avalonia.Themes.Default.Accents.BaseDark.xaml?assembly=Avalonia.Themes.Default") + Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/Base.xaml") + }, + new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) + { + Source = new Uri("avares://Avalonia.Themes.Fluent/Accents/BaseDark.xaml") + }, + new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) + { + Source = new Uri("avares://Avalonia.Themes.Default/Accents/BaseDark.xaml") }, new StyleInclude(new Uri("resm:Styles?assembly=ControlCatalog")) { - Source = new Uri("resm:Avalonia.Themes.Default.DefaultTheme.xaml?assembly=Avalonia.Themes.Default") + Source = new Uri("avares://Avalonia.Themes.Default/DefaultTheme.xaml") }, }; diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index fa4fd7dd07..af95e3c356 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -2,7 +2,6 @@ xmlns:pages="clr-namespace:ControlCatalog.Pages" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="ControlCatalog.MainView" - Background="Black" Foreground="{DynamicResource ThemeForegroundBrush}" FontSize="{DynamicResource FontSizeNormal}"> diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml index 08bb1584a9..304782dbf9 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml @@ -1,6 +1,30 @@ + + + + + + + + + + + + + + + + ItemsRepeater @@ -12,8 +36,6 @@ Stack - Horizontal UniformGrid - Vertical UniformGrid - Horizontal - WrapLayout - Horizontal - WrapLayout - Veritcal @@ -25,20 +47,8 @@ - - - - - - - - - - - - + diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs index a4003cadc9..9e898c4536 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs @@ -38,6 +38,12 @@ namespace ControlCatalog.Pages AvaloniaXamlLoader.Load(this); } + public void OnSelectTemplateKey(object sender, SelectTemplateEventArgs e) + { + var item = (ItemsRepeaterPageViewModel.Item)e.DataContext; + e.TemplateKey = (item.Index % 2 == 0) ? "even" : "odd"; + } + private void LayoutChanged(object sender, SelectionChangedEventArgs e) { if (_repeater == null) diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index 47b4ce7151..f4d81418ac 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -10,7 +10,13 @@ HorizontalAlignment="Center" Spacing="16"> - + diff --git a/samples/ControlCatalog/Pages/MenuPage.xaml b/samples/ControlCatalog/Pages/MenuPage.xaml index de9ea34e80..e9d2301e89 100644 --- a/samples/ControlCatalog/Pages/MenuPage.xaml +++ b/samples/ControlCatalog/Pages/MenuPage.xaml @@ -20,7 +20,9 @@ - + + + @@ -52,6 +54,33 @@ + + + + Mixed + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs b/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs index 73aaeff994..f893a6e28e 100644 --- a/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs @@ -62,13 +62,9 @@ namespace ControlCatalog.ViewModels public class Item : ReactiveObject { private double _height = double.NaN; - private int _index; - - public Item(int index) - { - _index = index; - } + public Item(int index) => Index = index; + public int Index { get; } public string Text { get; set; } public double Height @@ -76,8 +72,6 @@ namespace ControlCatalog.ViewModels get => _height; set => this.RaiseAndSetIfChanged(ref _height, value); } - - public IBrush Background => ((_index % 2) == 0) ? Brushes.Yellow : Brushes.Wheat; } } } diff --git a/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs b/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs index dc9c4a8f49..9e7ae8b716 100644 --- a/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MenuPageViewModel.cs @@ -17,6 +17,23 @@ namespace ControlCatalog.ViewModels SaveCommand = ReactiveCommand.Create(Save, Observable.Return(false)); OpenRecentCommand = ReactiveCommand.Create(OpenRecent); + var recentItems = new[] + { + new MenuItemViewModel + { + Header = "File1.txt", + Command = OpenRecentCommand, + CommandParameter = @"c:\foo\File1.txt" + }, + new MenuItemViewModel + { + Header = "File2.txt", + Command = OpenRecentCommand, + CommandParameter = @"c:\foo\File2.txt" + }, + }; + + RecentItems = recentItems; MenuItems = new[] { new MenuItemViewModel @@ -24,27 +41,13 @@ namespace ControlCatalog.ViewModels Header = "_File", Items = new[] { - new MenuItemViewModel { Header = "_Open...", Command = OpenCommand }, + new MenuItemViewModel { Header = "O_pen...", Command = OpenCommand }, new MenuItemViewModel { Header = "Save", Command = SaveCommand }, new MenuItemViewModel { Header = "-" }, new MenuItemViewModel { Header = "Recent", - Items = new[] - { - new MenuItemViewModel - { - Header = "File1.txt", - Command = OpenRecentCommand, - CommandParameter = @"c:\foo\File1.txt" - }, - new MenuItemViewModel - { - Header = "File2.txt", - Command = OpenRecentCommand, - CommandParameter = @"c:\foo\File2.txt" - }, - } + Items = recentItems }, } }, @@ -61,6 +64,7 @@ namespace ControlCatalog.ViewModels } public IReadOnlyList MenuItems { get; set; } + public IReadOnlyList RecentItems { get; set; } public ReactiveCommand OpenCommand { get; } public ReactiveCommand SaveCommand { get; } public ReactiveCommand OpenRecentCommand { get; } diff --git a/samples/RenderDemo/App.xaml b/samples/RenderDemo/App.xaml index ccc3f54cc0..61e4d2385b 100644 --- a/samples/RenderDemo/App.xaml +++ b/samples/RenderDemo/App.xaml @@ -3,8 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="RenderDemo.App"> - - + - \ No newline at end of file + diff --git a/samples/RenderDemo/SideBar.xaml b/samples/RenderDemo/SideBar.xaml index 07fdb91a16..fd23067f61 100644 --- a/samples/RenderDemo/SideBar.xaml +++ b/samples/RenderDemo/SideBar.xaml @@ -1,65 +1,68 @@ - + - - - - - + + + + + + diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 69fd2a9f13..71dce93ce7 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -126,7 +126,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform _view.Visibility = ViewStates.Visible; } - public double Scaling => 1; + public double RenderScaling => 1; void Draw() { diff --git a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs index 38a23f918f..40cf81358f 100644 --- a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs +++ b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs @@ -1,3 +1,5 @@ +using System; +using System.Runtime.ConstrainedExecution; using System.Threading; namespace Avalonia.Threading @@ -7,6 +9,20 @@ namespace Avalonia.Threading /// public class AvaloniaSynchronizationContext : SynchronizationContext { + public interface INonPumpingPlatformWaitProvider + { + int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout); + } + + private readonly INonPumpingPlatformWaitProvider _waitProvider; + + public AvaloniaSynchronizationContext(INonPumpingPlatformWaitProvider waitProvider) + { + _waitProvider = waitProvider; + if (_waitProvider != null) + SetWaitNotificationRequired(); + } + /// /// Controls if SynchronizationContext should be installed in InstallIfNeeded. Used by Designer. /// @@ -22,7 +38,8 @@ namespace Avalonia.Threading return; } - SetSynchronizationContext(new AvaloniaSynchronizationContext()); + SetSynchronizationContext(new AvaloniaSynchronizationContext(AvaloniaLocator.Current + .GetService())); } /// @@ -39,5 +56,13 @@ namespace Avalonia.Threading else Dispatcher.UIThread.InvokeAsync(() => d(state), DispatcherPriority.Send).Wait(); } + + [PrePrepareMethod] + public override int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout) + { + if (_waitProvider != null) + return _waitProvider.Wait(waitHandles, waitAll, millisecondsTimeout); + return base.Wait(waitHandles, waitAll, millisecondsTimeout); + } } } diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index 7117fd51a2..490364c0d8 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -17,14 +17,14 @@ Shared/AvaloniaResourceXamlInfo.cs - + XamlIlExtensions/%(RecursiveDir)%(FileName)%(Extension) - + XamlIl/%(RecursiveDir)%(FileName)%(Extension) - + XamlIl.Cecil/%(RecursiveDir)%(FileName)%(Extension) @@ -57,8 +57,8 @@ Markup/%(RecursiveDir)%(FileName)%(Extension) - - + + diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index fe9656a00b..7e921944ea 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -3922,6 +3922,12 @@ namespace Avalonia.Controls dataGridCell: editingCell); EditingRow.InvalidateDesiredHeight(); + var column = editingCell.OwningColumn; + if (column.Width.IsSizeToCells || column.Width.IsAuto) + {// Invalidate desired width and force recalculation + column.SetWidthDesiredValue(0); + EditingRow.OwningGrid.AutoSizeColumn(column, editingCell.DesiredSize.Width); + } } // We're done, so raise the CellEditEnded event diff --git a/src/Avalonia.Controls/Chrome/CaptionButtons.cs b/src/Avalonia.Controls/Chrome/CaptionButtons.cs index 75d6c366b8..a86cbc271b 100644 --- a/src/Avalonia.Controls/Chrome/CaptionButtons.cs +++ b/src/Avalonia.Controls/Chrome/CaptionButtons.cs @@ -38,12 +38,10 @@ namespace Avalonia.Controls.Chrome { if (_disposables != null) { - var layer = ChromeOverlayLayer.GetOverlayLayer(_hostWindow); - - layer?.Children.Remove(this); - _disposables.Dispose(); _disposables = null; + + _hostWindow = null; } } diff --git a/src/Avalonia.Controls/Chrome/TitleBar.cs b/src/Avalonia.Controls/Chrome/TitleBar.cs index 78b49d2a03..c0c8076dd8 100644 --- a/src/Avalonia.Controls/Chrome/TitleBar.cs +++ b/src/Avalonia.Controls/Chrome/TitleBar.cs @@ -12,106 +12,86 @@ namespace Avalonia.Controls.Chrome public class TitleBar : TemplatedControl { private CompositeDisposable? _disposables; - private readonly Window? _hostWindow; private CaptionButtons? _captionButtons; - public TitleBar(Window hostWindow) + private void UpdateSize(Window window) { - _hostWindow = hostWindow; - } - - public TitleBar() - { - - } - - public void Attach() - { - if (_disposables == null) - { - var layer = ChromeOverlayLayer.GetOverlayLayer(_hostWindow); - - layer?.Children.Add(this); - - if (_hostWindow != null) - { - _disposables = new CompositeDisposable - { - _hostWindow.GetObservable(Window.WindowDecorationMarginProperty) - .Subscribe(x => UpdateSize()), - - _hostWindow.GetObservable(Window.ExtendClientAreaTitleBarHeightHintProperty) - .Subscribe(x => UpdateSize()), - - _hostWindow.GetObservable(Window.OffScreenMarginProperty) - .Subscribe(x => UpdateSize()), - - _hostWindow.GetObservable(Window.WindowStateProperty) - .Subscribe(x => - { - PseudoClasses.Set(":minimized", x == WindowState.Minimized); - PseudoClasses.Set(":normal", x == WindowState.Normal); - PseudoClasses.Set(":maximized", x == WindowState.Maximized); - PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen); - }) - }; - - _captionButtons?.Attach(_hostWindow); - } - - UpdateSize(); - } - } - - private void UpdateSize() - { - if (_hostWindow != null) + if (window != null) { Margin = new Thickness( - _hostWindow.OffScreenMargin.Left, - _hostWindow.OffScreenMargin.Top, - _hostWindow.OffScreenMargin.Right, - _hostWindow.OffScreenMargin.Bottom); + window.OffScreenMargin.Left, + window.OffScreenMargin.Top, + window.OffScreenMargin.Right, + window.OffScreenMargin.Bottom); - if (_hostWindow.WindowState != WindowState.FullScreen) + if (window.WindowState != WindowState.FullScreen) { - Height = _hostWindow.WindowDecorationMargin.Top; + Height = window.WindowDecorationMargin.Top; if (_captionButtons != null) { _captionButtons.Height = Height; } } + + IsVisible = window.PlatformImpl.NeedsManagedDecorations; } } - public void Detach() + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { - if (_disposables != null) - { - var layer = ChromeOverlayLayer.GetOverlayLayer(_hostWindow); - - layer?.Children.Remove(this); + base.OnApplyTemplate(e); - _disposables.Dispose(); - _disposables = null; + _captionButtons?.Detach(); + + _captionButtons = e.NameScope.Get("PART_CaptionButtons"); - _captionButtons?.Detach(); + if (VisualRoot is Window window) + { + _captionButtons?.Attach(window); + + UpdateSize(window); } } - protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { - base.OnApplyTemplate(e); - - _captionButtons = e.NameScope.Get("PART_CaptionButtons"); + base.OnAttachedToVisualTree(e); - if (_hostWindow != null) + if (VisualRoot is Window window) { - _captionButtons.Attach(_hostWindow); + _disposables = new CompositeDisposable + { + window.GetObservable(Window.WindowDecorationMarginProperty) + .Subscribe(x => UpdateSize(window)), + window.GetObservable(Window.ExtendClientAreaTitleBarHeightHintProperty) + .Subscribe(x => UpdateSize(window)), + window.GetObservable(Window.OffScreenMarginProperty) + .Subscribe(x => UpdateSize(window)), + window.GetObservable(Window.ExtendClientAreaChromeHintsProperty) + .Subscribe(x => UpdateSize(window)), + window.GetObservable(Window.WindowStateProperty) + .Subscribe(x => + { + PseudoClasses.Set(":minimized", x == WindowState.Minimized); + PseudoClasses.Set(":normal", x == WindowState.Normal); + PseudoClasses.Set(":maximized", x == WindowState.Maximized); + PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen); + }), + window.GetObservable(Window.IsExtendedIntoWindowDecorationsProperty) + .Subscribe(x => UpdateSize(window)) + }; } + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); - UpdateSize(); + _disposables?.Dispose(); + + _captionButtons?.Detach(); + _captionButtons = null; } } } diff --git a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs index 5d3311e8c6..a41c159980 100644 --- a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs @@ -88,7 +88,7 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterDirect(nameof(SelectedDate), x => x.SelectedDate, (x, v) => x.SelectedDate = v); - //Template Items + // Template Items private Button _flyoutButton; private TextBlock _dayText; private TextBlock _monthText; @@ -359,10 +359,14 @@ namespace Avalonia.Controls } } - Grid.SetColumn(_spacer1, 1); - Grid.SetColumn(_spacer2, 3); - _spacer1.IsVisible = columnIndex > 1; - _spacer2.IsVisible = columnIndex > 2; + var isSpacer1Visible = columnIndex > 1; + var isSpacer2Visible = columnIndex > 2; + // ternary conditional operator is used to make sure grid cells will be validated + Grid.SetColumn(_spacer1, isSpacer1Visible ? 1 : 0); + Grid.SetColumn(_spacer2, isSpacer2Visible ? 3 : 0); + + _spacer1.IsVisible = isSpacer1Visible; + _spacer2.IsVisible = isSpacer2Visible; } private void SetSelectedDateText() @@ -398,7 +402,7 @@ namespace Avalonia.Controls var deltaY = _presenter.GetOffsetForPopup(); - //The extra 5 px I think is related to default popup placement behavior + // The extra 5 px I think is related to default popup placement behavior _popup.Host.ConfigurePosition(_popup.PlacementTarget, PlacementMode.AnchorAndGravity, new Point(0, deltaY + 5), Primitives.PopupPositioning.PopupAnchor.Bottom, Primitives.PopupPositioning.PopupGravity.Bottom, Primitives.PopupPositioning.PopupPositionerConstraintAdjustment.SlideY); diff --git a/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs b/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs index 8b86e46e88..31527ccb16 100644 --- a/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs +++ b/src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs @@ -77,7 +77,7 @@ namespace Avalonia.Controls DatePicker.YearVisibleProperty.AddOwner(x => x.YearVisible, (x, v) => x.YearVisible = v); - //Template Items + // Template Items private Grid _pickerContainer; private Button _acceptButton; private Button _dismissButton; @@ -107,7 +107,7 @@ namespace Avalonia.Controls private bool _yearVisible = true; private DateTimeOffset _syncDate; - private GregorianCalendar _calendar; + private readonly GregorianCalendar _calendar; private bool _suppressUpdateSelection; public DatePickerPresenter() @@ -234,7 +234,7 @@ namespace Avalonia.Controls protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); - //These are requirements, so throw if not found + // These are requirements, so throw if not found _pickerContainer = e.NameScope.Get("PickerContainer"); _monthHost = e.NameScope.Get("MonthHost"); _dayHost = e.NameScope.Get("DayHost"); @@ -326,7 +326,7 @@ namespace Avalonia.Controls /// private void InitPicker() { - //OnApplyTemplate must've been called before we can init here... + // OnApplyTemplate must've been called before we can init here... if (_pickerContainer == null) return; @@ -344,12 +344,11 @@ namespace Avalonia.Controls SetGrid(); - //Date should've been set when we reach this point + // Date should've been set when we reach this point var dt = Date; if (DayVisible) { - GregorianCalendar gc = new GregorianCalendar(); - var maxDays = gc.GetDaysInMonth(dt.Year, dt.Month); + var maxDays = _calendar.GetDaysInMonth(dt.Year, dt.Month); _daySelector.MaximumValue = maxDays; _daySelector.MinimumValue = 1; _daySelector.SelectedValue = dt.Day; @@ -407,10 +406,14 @@ namespace Avalonia.Controls } } - Grid.SetColumn(_spacer1, 1); - Grid.SetColumn(_spacer2, 3); - _spacer1.IsVisible = columnIndex > 1; - _spacer2.IsVisible = columnIndex > 2; + var isSpacer1Visible = columnIndex > 1; + var isSpacer2Visible = columnIndex > 2; + // ternary conditional operator is used to make sure grid cells will be validated + Grid.SetColumn(_spacer1, isSpacer1Visible ? 1 : 0); + Grid.SetColumn(_spacer2, isSpacer2Visible ? 3 : 0); + + _spacer1.IsVisible = isSpacer1Visible; + _spacer2.IsVisible = isSpacer2Visible; } private void SetInitialFocus() @@ -433,12 +436,12 @@ namespace Avalonia.Controls } } - private void OnDismissButtonClicked(object sender, Avalonia.Interactivity.RoutedEventArgs e) + private void OnDismissButtonClicked(object sender, RoutedEventArgs e) { OnDismiss(); } - private void OnAcceptButtonClicked(object sender, Avalonia.Interactivity.RoutedEventArgs e) + private void OnAcceptButtonClicked(object sender, RoutedEventArgs e) { Date = _syncDate; OnConfirmed(); @@ -471,7 +474,7 @@ namespace Avalonia.Controls _syncDate = newDate; - //We don't need to update the days if not displaying day, not february + // We don't need to update the days if not displaying day, not february if (!DayVisible || _syncDate.Month != 2) return; diff --git a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs index 3fa426b1d3..e54da1fb3a 100644 --- a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs @@ -15,7 +15,7 @@ namespace Avalonia.Controls /// Defines the property /// public static readonly DirectProperty MinuteIncrementProperty = - AvaloniaProperty.RegisterDirect(nameof(MinuteIncrement), + AvaloniaProperty.RegisterDirect(nameof(MinuteIncrement), x => x.MinuteIncrement, (x, v) => x.MinuteIncrement = v); /// @@ -34,17 +34,17 @@ namespace Avalonia.Controls /// Defines the property /// public static readonly DirectProperty ClockIdentifierProperty = - AvaloniaProperty.RegisterDirect(nameof(ClockIdentifier), + AvaloniaProperty.RegisterDirect(nameof(ClockIdentifier), x => x.ClockIdentifier, (x, v) => x.ClockIdentifier = v); /// /// Defines the property /// public static readonly DirectProperty SelectedTimeProperty = - AvaloniaProperty.RegisterDirect(nameof(SelectedTime), + AvaloniaProperty.RegisterDirect(nameof(SelectedTime), x => x.SelectedTime, (x, v) => x.SelectedTime = v); - //Template Items + // Template Items private TimePickerPresenter _presenter; private Button _flyoutButton; private Border _firstPickerHost; @@ -52,7 +52,7 @@ namespace Avalonia.Controls private Border _thirdPickerHost; private TextBlock _hourText; private TextBlock _minuteText; - public TextBlock _periodText; + private TextBlock _periodText; private Rectangle _firstSplitter; private Rectangle _secondSplitter; private Grid _contentGrid; @@ -145,7 +145,7 @@ namespace Avalonia.Controls if (_flyoutButton != null) _flyoutButton.Click -= OnFlyoutButtonClicked; - if(_presenter != null) + if (_presenter != null) { _presenter.Confirmed -= OnConfirmed; _presenter.Dismissed -= OnDismissPicker; @@ -170,7 +170,6 @@ namespace Avalonia.Controls _popup = e.NameScope.Find("Popup"); _presenter = e.NameScope.Find("PickerPresenter"); - if (_flyoutButton != null) _flyoutButton.Click += OnFlyoutButtonClicked; @@ -185,7 +184,6 @@ namespace Avalonia.Controls _presenter[!TimePickerPresenter.MinuteIncrementProperty] = this[!MinuteIncrementProperty]; _presenter[!TimePickerPresenter.ClockIdentifierProperty] = this[!ClockIdentifierProperty]; } - } private void SetGrid() @@ -195,30 +193,19 @@ namespace Avalonia.Controls bool use24HourClock = ClockIdentifier == "24HourClock"; - if (!use24HourClock) - { - _contentGrid.ColumnDefinitions = new ColumnDefinitions("*,Auto,*,Auto,*"); - _thirdPickerHost.IsVisible = true; - _secondSplitter.IsVisible = true; + var columnsD = use24HourClock ? "*, Auto, *" : "*, Auto, *, Auto, *"; + _contentGrid.ColumnDefinitions = new ColumnDefinitions(columnsD); - Grid.SetColumn(_firstPickerHost, 0); - Grid.SetColumn(_secondPickerHost, 2); - Grid.SetColumn(_thirdPickerHost, 4); + _thirdPickerHost.IsVisible = !use24HourClock; + _secondSplitter.IsVisible = !use24HourClock; - Grid.SetColumn(_firstSplitter, 1); - Grid.SetColumn(_secondSplitter, 3); - } - else - { - _contentGrid.ColumnDefinitions = new ColumnDefinitions("*,Auto,*"); - _thirdPickerHost.IsVisible = false; - _secondSplitter.IsVisible = false; + Grid.SetColumn(_firstPickerHost, 0); + Grid.SetColumn(_secondPickerHost, 2); - Grid.SetColumn(_firstPickerHost, 0); - Grid.SetColumn(_secondPickerHost, 2); + Grid.SetColumn(_thirdPickerHost, use24HourClock ? 0 : 4); - Grid.SetColumn(_firstSplitter, 1); - } + Grid.SetColumn(_firstSplitter, 1); + Grid.SetColumn(_secondSplitter, use24HourClock ? 0 : 3); } private void SetSelectedTimeText() @@ -237,14 +224,13 @@ namespace Avalonia.Controls hr = hr > 12 ? hr - 12 : hr == 0 ? 12 : hr; newTime = new TimeSpan(hr, newTime.Minutes, 0); } - _hourText.Text = newTime.ToString("%h"); + _hourText.Text = newTime.ToString("%h"); _minuteText.Text = newTime.ToString("mm"); PseudoClasses.Set(":hasnotime", false); _periodText.Text = time.Value.Hours >= 12 ? CultureInfo.CurrentCulture.DateTimeFormat.PMDesignator : CultureInfo.CurrentCulture.DateTimeFormat.AMDesignator; - } else { @@ -262,7 +248,7 @@ namespace Avalonia.Controls SelectedTimeChanged?.Invoke(this, new TimePickerSelectedValueChangedEventArgs(oldTime, newTime)); } - private void OnFlyoutButtonClicked(object sender, Avalonia.Interactivity.RoutedEventArgs e) + private void OnFlyoutButtonClicked(object sender, Interactivity.RoutedEventArgs e) { _presenter.Time = SelectedTime ?? DateTime.Now.TimeOfDay; @@ -270,7 +256,7 @@ namespace Avalonia.Controls var deltaY = _presenter.GetOffsetForPopup(); - //The extra 5 px I think is related to default popup placement behavior + // The extra 5 px I think is related to default popup placement behavior _popup.Host.ConfigurePosition(_popup.PlacementTarget, PlacementMode.AnchorAndGravity, new Point(0, deltaY + 5), Primitives.PopupPositioning.PopupAnchor.Bottom, Primitives.PopupPositioning.PopupGravity.Bottom, Primitives.PopupPositioning.PopupPositionerConstraintAdjustment.SlideY); @@ -287,6 +273,5 @@ namespace Avalonia.Controls _popup.Close(); SelectedTime = _presenter.Time; } - } } diff --git a/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs b/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs index 5ab5b92e16..920eeeb406 100644 --- a/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs +++ b/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs @@ -39,7 +39,7 @@ namespace Avalonia.Controls KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Cycle); } - //TemplateItems + // TemplateItems private Grid _pickerContainer; private Button _acceptButton; private Button _dismissButton; @@ -55,8 +55,8 @@ namespace Avalonia.Controls private Button _minuteDownButton; private Button _periodDownButton; - //Backing Fields - private TimeSpan _Time; + // Backing Fields + private TimeSpan _time; private int _minuteIncrement = 1; private string _clockIdentifier = "12HourClock"; @@ -83,7 +83,7 @@ namespace Avalonia.Controls get => _clockIdentifier; set { - if (string.IsNullOrEmpty(value) || value == "" || !(value == "12HourClock" || value == "24HourClock")) + if (string.IsNullOrEmpty(value) || !(value == "12HourClock" || value == "24HourClock")) throw new ArgumentException("Invalid ClockIdentifier"); SetAndRaise(ClockIdentifierProperty, ref _clockIdentifier, value); InitPicker(); @@ -95,10 +95,10 @@ namespace Avalonia.Controls /// public TimeSpan Time { - get => _Time; + get => _time; set { - SetAndRaise(TimeProperty, ref _Time, value); + SetAndRaise(TimeProperty, ref _time, value); InitPicker(); } } @@ -213,26 +213,24 @@ namespace Avalonia.Controls private void SetGrid() { - if (ClockIdentifier == "12HourClock") - { - _pickerContainer.ColumnDefinitions = new ColumnDefinitions("*,Auto,*,Auto,*"); - _spacer2.IsVisible = true; - _periodHost.IsVisible = true; - } - else - { - _pickerContainer.ColumnDefinitions = new ColumnDefinitions("*,Auto,*"); - _spacer2.IsVisible = false; - _periodHost.IsVisible = false; - } + bool use24HourClock = ClockIdentifier == "24HourClock"; + + var columnsD = use24HourClock ? "*, Auto, *" : "*, Auto, *, Auto, *"; + _pickerContainer.ColumnDefinitions = new ColumnDefinitions(columnsD); + + _spacer2.IsVisible = !use24HourClock; + _periodHost.IsVisible = !use24HourClock; + + Grid.SetColumn(_spacer2, use24HourClock ? 0 : 3); + Grid.SetColumn(_periodHost, use24HourClock ? 0 : 4); } - private void OnDismissButtonClicked(object sender, Avalonia.Interactivity.RoutedEventArgs e) + private void OnDismissButtonClicked(object sender, RoutedEventArgs e) { OnDismiss(); } - private void OnAcceptButtonClicked(object sender, Avalonia.Interactivity.RoutedEventArgs e) + private void OnAcceptButtonClicked(object sender, RoutedEventArgs e) { OnConfirmed(); } diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs index f8bd2878d9..522103c7bd 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs @@ -35,7 +35,7 @@ namespace Avalonia.Controls.Embedding.Offscreen } } - public double Scaling + public double RenderScaling { get { return _scaling; } set diff --git a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs index cd1ce3deae..9e65ef5f81 100644 --- a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs @@ -142,7 +142,6 @@ namespace Avalonia.Controls.Generators private readonly IDataTemplate _inner; public WrapperTreeDataTemplate(IDataTemplate inner) => _inner = inner; public IControl Build(object param) => _inner.Build(param); - public bool SupportsRecycling => _inner.SupportsRecycling; public bool Match(object data) => _inner.Match(data); public InstancedBinding ItemsSelector(object item) => null; } diff --git a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs index 0d77cbf802..7514f214aa 100644 --- a/src/Avalonia.Controls/Platform/ITopLevelImpl.cs +++ b/src/Avalonia.Controls/Platform/ITopLevelImpl.cs @@ -23,10 +23,10 @@ namespace Avalonia.Platform Size ClientSize { get; } /// - /// Gets the scaling factor for the toplevel. + /// Gets the scaling factor for the toplevel. This is used for rendering. /// - double Scaling { get; } - + double RenderScaling { get; } + /// /// The list of native platform's surfaces that can be consumed by rendering subsystems. /// diff --git a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs index b190c4f2e7..ecaf87d1ed 100644 --- a/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs +++ b/src/Avalonia.Controls/Platform/IWindowBaseImpl.cs @@ -13,6 +13,11 @@ namespace Avalonia.Platform /// Hides the window. /// void Hide(); + + /// + /// Gets the scaling factor for Window positioning and sizing. + /// + double DesktopScaling { get; } /// /// Gets the position of the window in device pixels. diff --git a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs index 70a7583daf..7888249bdd 100644 --- a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs +++ b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs @@ -155,6 +155,11 @@ namespace Avalonia.Controls.Presenters } } + protected override void PanelCreated(IPanel panel) + { + ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); + } + /// /// Moves to the selected page, animating if a is set. /// diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index c4571505ba..8837901816 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -86,7 +86,7 @@ namespace Avalonia.Controls.Presenters private IControl _child; private bool _createdChild; - private IDataTemplate _dataTemplate; + private IRecyclingDataTemplate _recyclingDataTemplate; private readonly BorderRenderHelper _borderRenderer = new BorderRenderHelper(); /// @@ -281,7 +281,7 @@ namespace Avalonia.Controls.Presenters protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) { base.OnAttachedToLogicalTree(e); - _dataTemplate = null; + _recyclingDataTemplate = null; _createdChild = false; InvalidateMeasure(); } @@ -307,22 +307,21 @@ namespace Avalonia.Controls.Presenters { var dataTemplate = this.FindDataTemplate(content, ContentTemplate) ?? FuncDataTemplate.Default; - // We have content and it isn't a control, so if the new data template is the same - // as the old data template, try to recycle the existing child control to display - // the new data. - if (dataTemplate == _dataTemplate && dataTemplate.SupportsRecycling) + if (dataTemplate is IRecyclingDataTemplate rdt) { - newChild = oldChild; + var toRecycle = rdt == _recyclingDataTemplate ? oldChild : null; + newChild = rdt.Build(content, toRecycle); + _recyclingDataTemplate = rdt; } else { - _dataTemplate = dataTemplate; - newChild = _dataTemplate.Build(content); + newChild = dataTemplate.Build(content); + _recyclingDataTemplate = null; } } else { - _dataTemplate = null; + _recyclingDataTemplate = null; } return newChild; @@ -422,7 +421,7 @@ namespace Avalonia.Controls.Presenters LogicalChildren.Remove(Child); ((ISetInheritanceParent)Child).SetParent(Child.Parent); Child = null; - _dataTemplate = null; + _recyclingDataTemplate = null; } InvalidateMeasure(); diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index 23846bcd2e..52f173fc71 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -229,8 +229,6 @@ namespace Avalonia.Controls.Presenters } PanelCreated(Panel); - - ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } /// diff --git a/src/Avalonia.Controls/Primitives/ChromeOverlayLayer.cs b/src/Avalonia.Controls/Primitives/ChromeOverlayLayer.cs index ba0fdfd535..7171ecc302 100644 --- a/src/Avalonia.Controls/Primitives/ChromeOverlayLayer.cs +++ b/src/Avalonia.Controls/Primitives/ChromeOverlayLayer.cs @@ -8,7 +8,7 @@ namespace Avalonia.Controls.Primitives { public class ChromeOverlayLayer : Panel, ICustomSimpleHitTest { - public static ChromeOverlayLayer? GetOverlayLayer(IVisual visual) + public static Panel? GetOverlayLayer(IVisual visual) { foreach (var v in visual.GetVisualAncestors()) if (v is VisualLayerManager vlm) @@ -24,6 +24,11 @@ namespace Avalonia.Controls.Primitives return null; } + public void Add(Control c) + { + base.Children.Add(c); + } + public bool HitTest(Point point) => Children.HitTestCustom(point); } } diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs index b0e3d1ab08..91ed5d975d 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs @@ -40,9 +40,9 @@ namespace Avalonia.Controls.Primitives.PopupPositioning public void MoveAndResize(Point devicePoint, Size virtualSize) { - _moveResize(new PixelPoint((int)devicePoint.X, (int)devicePoint.Y), virtualSize, _parent.Scaling); + _moveResize(new PixelPoint((int)devicePoint.X, (int)devicePoint.Y), virtualSize, _parent.RenderScaling); } - public virtual double Scaling => _parent.Scaling; + public virtual double Scaling => _parent.DesktopScaling; } } diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index c915dc70b6..59b7777b1b 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -692,14 +692,24 @@ namespace Avalonia.Controls.Primitives } } - foreach (var i in e.SelectedIndices) + if (e.SelectedIndices.Count > 0 || e.DeselectedIndices.Count > 0) { - Mark(i.GetAt(0), true); - } + foreach (var i in e.SelectedIndices) + { + Mark(i.GetAt(0), true); + } - foreach (var i in e.DeselectedIndices) + foreach (var i in e.DeselectedIndices) + { + Mark(i.GetAt(0), false); + } + } + else if (e.DeselectedItems.Count > 0) { - Mark(i.GetAt(0), false); + // (De)selected indices being empty means that a selected item was removed from + // the Items (it can't tell us the index of the item because the index is no longer + // valid). In this case, we just update the selection state of all containers. + UpdateContainerSelection(); } var newSelectedIndex = SelectedIndex; diff --git a/src/Avalonia.Controls/Primitives/VisualLayerManager.cs b/src/Avalonia.Controls/Primitives/VisualLayerManager.cs index 3084d7fa72..ff83351190 100644 --- a/src/Avalonia.Controls/Primitives/VisualLayerManager.cs +++ b/src/Avalonia.Controls/Primitives/VisualLayerManager.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Avalonia.LogicalTree; +using Avalonia.Metadata; namespace Avalonia.Controls.Primitives { @@ -11,10 +12,12 @@ namespace Avalonia.Controls.Primitives private ILogicalRoot _logicalRoot; private readonly List _layers = new List(); - + + public static readonly StyledProperty ChromeOverlayLayerProperty = + AvaloniaProperty.Register(nameof(ChromeOverlayLayer)); public bool IsPopup { get; set; } - + public AdornerLayer AdornerLayer { get @@ -30,10 +33,19 @@ namespace Avalonia.Controls.Primitives { get { - var rv = FindLayer(); - if (rv == null) - AddLayer(rv = new ChromeOverlayLayer(), ChromeZIndex); - return rv; + var current = GetValue(ChromeOverlayLayerProperty); + + if (current is null) + { + var chromeOverlayLayer = new ChromeOverlayLayer(); + AddLayer(chromeOverlayLayer, ChromeZIndex); + + SetValue(ChromeOverlayLayerProperty, chromeOverlayLayer); + + current = chromeOverlayLayer; + } + + return current; } } @@ -44,7 +56,7 @@ namespace Avalonia.Controls.Primitives if (IsPopup) return null; var rv = FindLayer(); - if(rv == null) + if (rv == null) AddLayer(rv = new OverlayLayer(), OverlayZIndex); return rv; } @@ -65,7 +77,8 @@ namespace Avalonia.Controls.Primitives layer.ZIndex = zindex; VisualChildren.Add(layer); if (((ILogical)this).IsAttachedToLogicalTree) - ((ILogical)layer).NotifyAttachedToLogicalTree(new LogicalTreeAttachmentEventArgs(_logicalRoot, layer, this)); + ((ILogical)layer).NotifyAttachedToLogicalTree( + new LogicalTreeAttachmentEventArgs(_logicalRoot, layer, this)); InvalidateArrange(); } diff --git a/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs b/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs index f93de5ca15..f64c26682b 100644 --- a/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs +++ b/src/Avalonia.Controls/RelativePanel.AttachedProperties.cs @@ -33,7 +33,7 @@ namespace Avalonia.Controls AlignVerticalCenterWithProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); BelowProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); LeftOfProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); - LeftOfProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); + RightOfProperty.Changed.AddClassHandler(OnAlignPropertiesChanged); } /// diff --git a/src/Avalonia.Controls/RelativePanel.cs b/src/Avalonia.Controls/RelativePanel.cs index 033a5559f5..27f13a3f57 100644 --- a/src/Avalonia.Controls/RelativePanel.cs +++ b/src/Avalonia.Controls/RelativePanel.cs @@ -1,4 +1,4 @@ -/// Ported from https://github.com/HandyOrg/HandyControl/blob/master/src/Shared/HandyControl_Shared/Controls/Panel/RelativePanel.cs +// Ported from https://github.com/HandyOrg/HandyControl/blob/master/src/Shared/HandyControl_Shared/Controls/Panel/RelativePanel.cs using System; using System.Collections.Generic; using System.Linq; @@ -14,25 +14,28 @@ namespace Avalonia.Controls public RelativePanel() => _childGraph = new Graph(); - protected override Size MeasureOverride(Size availableSize) + private Layoutable? GetDependencyElement(AvaloniaProperty property, AvaloniaObject child) { - foreach (var child in Children) + var dependency = child.GetValue(property); + + if (dependency is Layoutable layoutable) { - child?.Measure(availableSize); + if (Children.Contains((ILayoutable)layoutable)) + return layoutable; + + throw new ArgumentException($"RelativePanel error: Element does not exist in the current context: {property.Name}"); } - return availableSize; + return null; } - protected override Size ArrangeOverride(Size arrangeSize) + protected override Size MeasureOverride(Size availableSize) { - _childGraph.Reset(arrangeSize); - - foreach (var child in Children.OfType()) + _childGraph.Clear(); + foreach (Layoutable child in Children) { if (child == null) continue; - var node = _childGraph.AddNode(child); node.AlignLeftWithNode = _childGraph.AddLink(node, GetDependencyElement(AlignLeftWithProperty, child)); @@ -47,63 +50,44 @@ namespace Avalonia.Controls node.AlignHorizontalCenterWith = _childGraph.AddLink(node, GetDependencyElement(AlignHorizontalCenterWithProperty, child)); node.AlignVerticalCenterWith = _childGraph.AddLink(node, GetDependencyElement(AlignVerticalCenterWithProperty, child)); - } - if (_childGraph.CheckCyclic()) - { - throw new Exception("RelativePanel error: Circular dependency detected. Layout could not complete."); } + _childGraph.Measure(availableSize); - var size = new Size(); - - foreach (var child in Children) - { - if (child.Bounds.Bottom > size.Height) - { - size = size.WithHeight(child.Bounds.Bottom); - } + _childGraph.Reset(false); + var boundingSize = _childGraph.GetBoundingSize(Width.IsNaN(), Height.IsNaN()); + _childGraph.Reset(); + _childGraph.Measure(boundingSize); + return boundingSize; + } - if (child.Bounds.Right > size.Width) - { - size = size.WithWidth(child.Bounds.Right); - } - } + protected override Size ArrangeOverride(Size arrangeSize) + { + _childGraph.GetNodes().Do(node => node.Arrange(arrangeSize)); + return arrangeSize; + } - if (VerticalAlignment == VerticalAlignment.Stretch) - { - size = size.WithHeight(arrangeSize.Height); - } + private class GraphNode + { + public bool Measured { get; set; } - if (HorizontalAlignment == HorizontalAlignment.Stretch) - { - size = size.WithWidth(arrangeSize.Width); - } + public Layoutable Element { get; } - return size; - } + private bool HorizontalOffsetFlag { get; set; } - private Layoutable? GetDependencyElement(AvaloniaProperty property, AvaloniaObject child) - { - var dependency = child.GetValue(property); + private bool VerticalOffsetFlag { get; set; } - if (dependency is Layoutable layoutable) - { - if (Children.Contains((ILayoutable)layoutable)) - return layoutable; + private Size BoundingSize { get; set; } - throw new ArgumentException($"RelativePanel error: Element does not exist in the current context: {property.Name}"); - } + public Size OriginDesiredSize { get; set; } - return null; - } + public double Left { get; set; } = double.NaN; - private class GraphNode - { - public Point Position { get; set; } + public double Top { get; set; } = double.NaN; - public bool Arranged { get; set; } + public double Right { get; set; } = double.NaN; - public Layoutable Element { get; } + public double Bottom { get; set; } = double.NaN; public HashSet OutgoingNodes { get; } @@ -132,19 +116,106 @@ namespace Avalonia.Controls OutgoingNodes = new HashSet(); Element = element; } + + public void Arrange(Size arrangeSize) => Element.Arrange(new Rect(Left, Top, Math.Max(arrangeSize.Width - Left - Right, 0), Math.Max(arrangeSize.Height - Top - Bottom, 0))); + + public void Reset(bool clearPos) + { + if (clearPos) + { + Left = double.NaN; + Top = double.NaN; + Right = double.NaN; + Bottom = double.NaN; + } + + Measured = false; + } + + public Size GetBoundingSize() + { + if (Left < 0 || Top < 0) return default; + if (Measured) + return BoundingSize; + + if (!OutgoingNodes.Any()) + { + BoundingSize = Element.DesiredSize; + Measured = true; + } + else + { + BoundingSize = GetBoundingSize(this, Element.DesiredSize, OutgoingNodes); + Measured = true; + } + + return BoundingSize; + } + + private static Size GetBoundingSize(GraphNode prevNode, Size prevSize, IEnumerable nodes) + { + foreach (var node in nodes) + { + if (node.Measured || !node.OutgoingNodes.Any()) + { + if (prevNode.LeftOfNode != null && prevNode.LeftOfNode == node || + prevNode.RightOfNode != null && prevNode.RightOfNode == node) + { + prevSize = prevSize.WithWidth(prevSize.Width + node.BoundingSize.Width); + if (GetAlignHorizontalCenterWithPanel(node.Element) || node.HorizontalOffsetFlag) + { + prevSize = prevSize.WithWidth(prevSize.Width + prevNode.OriginDesiredSize.Width); + prevNode.HorizontalOffsetFlag = true; + } + if (node.VerticalOffsetFlag) + { + prevNode.VerticalOffsetFlag = true; + } + } + + if (prevNode.AboveNode != null && prevNode.AboveNode == node || + prevNode.BelowNode != null && prevNode.BelowNode == node) + { + prevSize = prevSize.WithHeight(prevSize.Height + node.BoundingSize.Height); + if (GetAlignVerticalCenterWithPanel(node.Element) || node.VerticalOffsetFlag) + { + prevSize = prevSize.WithHeight(prevSize.Height + node.OriginDesiredSize.Height); + prevNode.VerticalOffsetFlag = true; + } + if (node.HorizontalOffsetFlag) + { + prevNode.HorizontalOffsetFlag = true; + } + } + } + else + { + return GetBoundingSize(node, prevSize, node.OutgoingNodes); + } + } + + return prevSize; + } } private class Graph { private readonly Dictionary _nodeDic; - private Size _arrangeSize; + private Size AvailableSize { get; set; } + + public Graph() => _nodeDic = new Dictionary(); + + public IEnumerable GetNodes() => _nodeDic.Values; - public Graph() + public void Clear() { - _nodeDic = new Dictionary(); + AvailableSize = new Size(); + _nodeDic.Clear(); } + public void Reset(bool clearPos = true) => _nodeDic.Values.Do(node => node.Reset(clearPos)); + public GraphNode? AddLink(GraphNode from, Layoutable? to) { if (to == null) @@ -177,177 +248,289 @@ namespace Avalonia.Controls return _nodeDic[value]; } - public void Reset(Size arrangeSize) + public void Measure(Size availableSize) { - _arrangeSize = arrangeSize; - _nodeDic.Clear(); + AvailableSize = availableSize; + Measure(_nodeDic.Values, null); } - public bool CheckCyclic() => CheckCyclic(_nodeDic.Values, null); - - private bool CheckCyclic(IEnumerable nodes, HashSet? set) + private void Measure(IEnumerable nodes, HashSet? set) { - set ??= new HashSet(); + set ??= new HashSet(); foreach (var node in nodes) { - if (!node.Arranged && node.OutgoingNodes.Count == 0) + if (!node.Measured && !node.OutgoingNodes.Any()) { - ArrangeChild(node, true); + MeasureChild(node); continue; } - - if (node.OutgoingNodes.All(item => item.Arranged)) + + if (node.OutgoingNodes.All(item => item.Measured)) { - ArrangeChild(node); + MeasureChild(node); continue; } - + if (!set.Add(node.Element)) - return true; + throw new Exception("RelativePanel error: Circular dependency detected. Layout could not complete."); + + Measure(node.OutgoingNodes, set); - return CheckCyclic(node.OutgoingNodes, set); + if (!node.Measured) + { + MeasureChild(node); + } } - - return false; } - private void ArrangeChild(GraphNode node, bool ignoneSibling = false) + private void MeasureChild(GraphNode node) { var child = node.Element; - var childSize = child.DesiredSize; - var childPos = new Point(); + child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); + node.OriginDesiredSize = child.DesiredSize; - if (GetAlignHorizontalCenterWithPanel(child)) + var alignLeftWithPanel = GetAlignLeftWithPanel(child); + var alignTopWithPanel = GetAlignTopWithPanel(child); + var alignRightWithPanel = GetAlignRightWithPanel(child); + var alignBottomWithPanel = GetAlignBottomWithPanel(child); + + if (alignLeftWithPanel) + node.Left = 0; + if (alignTopWithPanel) + node.Top = 0; + if (alignRightWithPanel) + node.Right = 0; + if (alignBottomWithPanel) + node.Bottom = 0; + + if (node.AlignLeftWithNode != null) { - childPos = childPos.WithX((_arrangeSize.Width - childSize.Width) / 2); + node.Left = node.Left.IsNaN() ? node.AlignLeftWithNode.Left : node.AlignLeftWithNode.Left * 0.5; } - if (GetAlignVerticalCenterWithPanel(child)) + if (node.AlignTopWithNode != null) { - childPos = childPos.WithY((_arrangeSize.Height - childSize.Height) / 2); + node.Top = node.Top.IsNaN() ? node.AlignTopWithNode.Top : node.AlignTopWithNode.Top * 0.5; } - var alignLeftWithPanel = GetAlignLeftWithPanel(child); - var alignTopWithPanel = GetAlignTopWithPanel(child); - var alignRightWithPanel = GetAlignRightWithPanel(child); - var alignBottomWithPanel = GetAlignBottomWithPanel(child); + if (node.AlignRightWithNode != null) + { + node.Right = node.Right.IsNaN() + ? node.AlignRightWithNode.Right + : node.AlignRightWithNode.Right * 0.5; + } - if (!ignoneSibling) + if (node.AlignBottomWithNode != null) { - if (node.LeftOfNode != null) - { - childPos = childPos.WithX(node.LeftOfNode.Position.X - childSize.Width); - } + node.Bottom = node.Bottom.IsNaN() + ? node.AlignBottomWithNode.Bottom + : node.AlignBottomWithNode.Bottom * 0.5; + } - if (node.AboveNode != null) - { - childPos = childPos.WithY(node.AboveNode.Position.Y - childSize.Height); - } + var availableHeight = AvailableSize.Height - node.Top - node.Bottom; + if (availableHeight.IsNaN()) + { + availableHeight = AvailableSize.Height; - if (node.RightOfNode != null) + if (!node.Top.IsNaN() && node.Bottom.IsNaN()) { - childPos = childPos.WithX(node.RightOfNode.Position.X + node.RightOfNode.Element.DesiredSize.Width); + availableHeight -= node.Top; } - - if (node.BelowNode != null) + else if (node.Top.IsNaN() && !node.Bottom.IsNaN()) { - childPos = childPos.WithY(node.BelowNode.Position.Y + node.BelowNode.Element.DesiredSize.Height); + availableHeight -= node.Bottom; } + } + + var availableWidth = AvailableSize.Width - node.Left - node.Right; + if (availableWidth.IsNaN()) + { + availableWidth = AvailableSize.Width; - if (node.AlignHorizontalCenterWith != null) + if (!node.Left.IsNaN() && node.Right.IsNaN()) { - childPos = childPos.WithX(node.AlignHorizontalCenterWith.Position.X + - (node.AlignHorizontalCenterWith.Element.DesiredSize.Width - childSize.Width) / 2); + availableWidth -= node.Left; } - - if (node.AlignVerticalCenterWith != null) + else if (node.Left.IsNaN() && !node.Right.IsNaN()) { - childPos = childPos.WithY(node.AlignVerticalCenterWith.Position.Y + - (node.AlignVerticalCenterWith.Element.DesiredSize.Height - childSize.Height) / 2); + availableWidth -= node.Right; } + } - if (node.AlignLeftWithNode != null) + child.Measure(new Size(Math.Max(availableWidth, 0), Math.Max(availableHeight, 0))); + var childSize = child.DesiredSize; + + if (node.LeftOfNode != null && node.Left.IsNaN()) + { + node.Left = node.LeftOfNode.Left - childSize.Width; + } + + if (node.AboveNode != null && node.Top.IsNaN()) + { + node.Top = node.AboveNode.Top - childSize.Height; + } + + if (node.RightOfNode != null) + { + if (node.Right.IsNaN()) { - childPos = childPos.WithX(node.AlignLeftWithNode.Position.X); + node.Right = node.RightOfNode.Right - childSize.Width; } - if (node.AlignTopWithNode != null) + if (node.Left.IsNaN()) { - childPos = childPos.WithY(node.AlignTopWithNode.Position.Y); + node.Left = AvailableSize.Width - node.RightOfNode.Right; } + } - if (node.AlignRightWithNode != null) + if (node.BelowNode != null) + { + if (node.Bottom.IsNaN()) { - childPos = childPos.WithX(node.AlignRightWithNode.Element.DesiredSize.Width + node.AlignRightWithNode.Position.X - childSize.Width); + node.Bottom = node.BelowNode.Bottom - childSize.Height; } - if (node.AlignBottomWithNode != null) + if (node.Top.IsNaN()) { - childPos = childPos.WithY(node.AlignBottomWithNode.Element.DesiredSize.Height + node.AlignBottomWithNode.Position.Y - childSize.Height); + node.Top = AvailableSize.Height - node.BelowNode.Bottom; } } - if (alignLeftWithPanel) + if (node.AlignHorizontalCenterWith != null) { - if (node.AlignRightWithNode != null) - { - childPos = childPos.WithX((node.AlignRightWithNode.Element.DesiredSize.Width + node.AlignRightWithNode.Position.X - childSize.Width) / 2); - } + var halfWidthLeft = (AvailableSize.Width + node.AlignHorizontalCenterWith.Left - node.AlignHorizontalCenterWith.Right - childSize.Width) * 0.5; + var halfWidthRight = (AvailableSize.Width - node.AlignHorizontalCenterWith.Left + node.AlignHorizontalCenterWith.Right - childSize.Width) * 0.5; + + if (node.Left.IsNaN()) + node.Left = halfWidthLeft; else - { - childPos = childPos.WithX(0); - } + node.Left = (node.Left + halfWidthLeft) * 0.5; + + if (node.Right.IsNaN()) + node.Right = halfWidthRight; + else + node.Right = (node.Right + halfWidthRight) * 0.5; } - if (alignTopWithPanel) + if (node.AlignVerticalCenterWith != null) { - if (node.AlignBottomWithNode != null) - { - childPos = childPos.WithY((node.AlignBottomWithNode.Element.DesiredSize.Height + node.AlignBottomWithNode.Position.Y - childSize.Height) / 2); - } + var halfHeightTop = (AvailableSize.Height + node.AlignVerticalCenterWith.Top - node.AlignVerticalCenterWith.Bottom - childSize.Height) * 0.5; + var halfHeightBottom = (AvailableSize.Height - node.AlignVerticalCenterWith.Top + node.AlignVerticalCenterWith.Bottom - childSize.Height) * 0.5; + + if (node.Top.IsNaN()) + node.Top = halfHeightTop; else - { - childPos = childPos.WithY(0); - } + node.Top = (node.Top + halfHeightTop) * 0.5; + + if (node.Bottom.IsNaN()) + node.Bottom = halfHeightBottom; + else + node.Bottom = (node.Bottom + halfHeightBottom) * 0.5; } - if (alignRightWithPanel) + if (GetAlignHorizontalCenterWithPanel(child)) { - if (alignLeftWithPanel) - { - childPos = childPos.WithX((_arrangeSize.Width - childSize.Width) / 2); - } - else if (node.AlignLeftWithNode == null) - { - childPos = childPos.WithX(_arrangeSize.Width - childSize.Width); - } + var halfSubWidth = (AvailableSize.Width - childSize.Width) * 0.5; + + if (node.Left.IsNaN()) + node.Left = halfSubWidth; else - { - childPos = childPos.WithX((_arrangeSize.Width + node.AlignLeftWithNode.Position.X - childSize.Width) / 2); - } + node.Left = (node.Left + halfSubWidth) * 0.5; + + if (node.Right.IsNaN()) + node.Right = halfSubWidth; + else + node.Right = (node.Right + halfSubWidth) * 0.5; } - if (alignBottomWithPanel) + if (GetAlignVerticalCenterWithPanel(child)) { - if (alignTopWithPanel) - { - childPos = childPos.WithY((_arrangeSize.Height - childSize.Height) / 2); - } - else if (node.AlignTopWithNode == null) + var halfSubHeight = (AvailableSize.Height - childSize.Height) * 0.5; + + if (node.Top.IsNaN()) + node.Top = halfSubHeight; + else + node.Top = (node.Top + halfSubHeight) * 0.5; + + if (node.Bottom.IsNaN()) + node.Bottom = halfSubHeight; + else + node.Bottom = (node.Bottom + halfSubHeight) * 0.5; + } + + if (node.Left.IsNaN()) + { + if (!node.Right.IsNaN()) + node.Left = AvailableSize.Width - node.Right - childSize.Width; + else { - childPos = childPos.WithY(_arrangeSize.Height - childSize.Height); + node.Left = 0; + node.Right = AvailableSize.Width - childSize.Width; } + } + else if (!node.Left.IsNaN() && node.Right.IsNaN()) + { + node.Right = AvailableSize.Width - node.Left - childSize.Width; + } + + if (node.Top.IsNaN()) + { + if (!node.Bottom.IsNaN()) + node.Top = AvailableSize.Height - node.Bottom - childSize.Height; else { - childPos = childPos.WithY((_arrangeSize.Height + node.AlignTopWithNode.Position.Y - childSize.Height) / 2); + node.Top = 0; + node.Bottom = AvailableSize.Height - childSize.Height; } } + else if (!node.Top.IsNaN() && node.Bottom.IsNaN()) + { + node.Bottom = AvailableSize.Height - node.Top - childSize.Height; + } + + node.Measured = true; + } + + public Size GetBoundingSize(bool calcWidth, bool calcHeight) + { + var boundingSize = new Size(); + + foreach (var node in _nodeDic.Values) + { + var size = node.GetBoundingSize(); + boundingSize = boundingSize.WithWidth(Math.Max(boundingSize.Width, size.Width)); + boundingSize = boundingSize.WithHeight(Math.Max(boundingSize.Height, size.Height)); + } + + boundingSize = boundingSize.WithWidth(calcWidth ? boundingSize.Width : AvailableSize.Width); + boundingSize = boundingSize.WithHeight(calcHeight ? boundingSize.Height : AvailableSize.Height); + return boundingSize; + } + } + } + + internal static partial class Extensions + { + /// + /// Returns a value that indicates whether the specified value is not a number (). + /// + /// A double-precision floating-point number. + /// true if evaluates to ; otherwise, false. + public static bool IsNaN(this double d) + { + return double.IsNaN(d); + } - child.Arrange(new Rect(childPos.X, childPos.Y, childSize.Width, childSize.Height)); - node.Position = childPos; - node.Arranged = true; + public static IEnumerable Do(this IEnumerable source, Action predicate) + { + var enumerable = source as IList ?? source.ToList(); + foreach (var item in enumerable) + { + predicate.Invoke(item); } + + return enumerable; } } } diff --git a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs index f5c673d5f9..931c27c575 100644 --- a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs +++ b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs @@ -192,7 +192,7 @@ namespace Avalonia.Controls.Remote.Server GetAvaloniaInputModifiers(pressed.Modifiers))); }, DispatcherPriority.Input); } - if (obj is PointerPressedEventMessage released) + if (obj is PointerReleasedEventMessage released) { Dispatcher.UIThread.Post(() => { diff --git a/src/Avalonia.Controls/Repeater/ElementFactory.cs b/src/Avalonia.Controls/Repeater/ElementFactory.cs new file mode 100644 index 0000000000..644e077221 --- /dev/null +++ b/src/Avalonia.Controls/Repeater/ElementFactory.cs @@ -0,0 +1,27 @@ +using Avalonia.Controls.Templates; + +namespace Avalonia.Controls +{ + public abstract class ElementFactory : IElementFactory + { + public IControl Build(object data) + { + return GetElementCore(new ElementFactoryGetArgs { Data = data }); + } + + public IControl GetElement(ElementFactoryGetArgs args) + { + return GetElementCore(args); + } + + public bool Match(object data) => true; + + public void RecycleElement(ElementFactoryRecycleArgs args) + { + RecycleElementCore(args); + } + + protected abstract IControl GetElementCore(ElementFactoryGetArgs args); + protected abstract void RecycleElementCore(ElementFactoryRecycleArgs args); + } +} diff --git a/src/Avalonia.Controls/Repeater/IElementFactory.cs b/src/Avalonia.Controls/Repeater/IElementFactory.cs new file mode 100644 index 0000000000..6a899a6f26 --- /dev/null +++ b/src/Avalonia.Controls/Repeater/IElementFactory.cs @@ -0,0 +1,66 @@ +using Avalonia.Controls.Templates; + +namespace Avalonia.Controls +{ + /// + /// Represents the optional arguments to use when calling an implementation of the + /// 's method. + /// + public class ElementFactoryGetArgs + { + /// + /// Gets or sets the data item for which an appropriate element tree should be realized + /// when calling . + /// + public object Data { get; set; } + + /// + /// Gets or sets the that is expected to be the parent of the + /// realized element from . + /// + public IControl Parent { get; set; } + + /// + /// Gets or sets the index of the item that should be realized. + /// + public int Index { get; set; } + } + + /// + /// Represents the optional arguments to use when calling an implementation of the + /// 's method. + /// + public class ElementFactoryRecycleArgs + { + /// + /// Gets or sets the to recycle when calling + /// . + /// + public IControl Element { get; set; } + + /// + /// Gets or sets the that is expected to be the parent of the + /// realized element from . + /// + public IControl Parent { get; set; } + } + + /// + /// A data template that supports creating and recyling elements for an . + /// + public interface IElementFactory : IDataTemplate + { + /// + /// Gets an . + /// + /// The element args. + public IControl GetElement(ElementFactoryGetArgs args); + + /// + /// Recycles an that was previously retrieved using + /// . + /// + /// The recycle args. + public void RecycleElement(ElementFactoryRecycleArgs args); + } +} diff --git a/src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs b/src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs index 04d859c742..dd97cde218 100644 --- a/src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs +++ b/src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs @@ -7,13 +7,26 @@ using Avalonia.Controls.Templates; namespace Avalonia.Controls { - internal class ItemTemplateWrapper + internal class ItemTemplateWrapper : IElementFactory { private readonly IDataTemplate _dataTemplate; public ItemTemplateWrapper(IDataTemplate dataTemplate) => _dataTemplate = dataTemplate; - public IControl GetElement(IControl parent, object data) + public IControl Build(object param) => GetElement(null, param); + public bool Match(object data) => _dataTemplate.Match(data); + + public IControl GetElement(ElementFactoryGetArgs args) + { + return GetElement(args.Parent, args.Data); + } + + public void RecycleElement(ElementFactoryRecycleArgs args) + { + RecycleElement(args.Parent, args.Element); + } + + private IControl GetElement(IControl parent, object data) { var selectedTemplate = _dataTemplate; var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate); @@ -37,7 +50,7 @@ namespace Avalonia.Controls return element; } - public void RecycleElement(IControl parent, IControl element) + private void RecycleElement(IControl parent, IControl element) { var selectedTemplate = _dataTemplate; var recyclePool = RecyclePool.GetPoolInstance(selectedTemplate); diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs index 87f4760156..8bc356bdec 100644 --- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs +++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs @@ -141,7 +141,7 @@ namespace Avalonia.Controls /// public ItemsSourceView ItemsSourceView { get; private set; } - internal ItemTemplateWrapper ItemTemplateShim { get; set; } + internal IElementFactory ItemTemplateShim { get; set; } internal Point LayoutOrigin { get; set; } internal object LayoutState { get; set; } internal IControl MadeAnchor => _viewportManager.MadeAnchor; @@ -664,7 +664,7 @@ namespace Avalonia.Controls } } - ItemTemplateShim = new ItemTemplateWrapper(newValue); + ItemTemplateShim = newValue as IElementFactory ?? new ItemTemplateWrapper(newValue); InvalidateMeasure(); } diff --git a/src/Avalonia.Controls/Repeater/RecyclePool.cs b/src/Avalonia.Controls/Repeater/RecyclePool.cs index 4e5950bdc5..28f299043c 100644 --- a/src/Avalonia.Controls/Repeater/RecyclePool.cs +++ b/src/Avalonia.Controls/Repeater/RecyclePool.cs @@ -11,10 +11,13 @@ using Avalonia.Controls.Templates; namespace Avalonia.Controls { - internal class RecyclePool + public class RecyclePool { - public static readonly AttachedProperty OriginTemplateProperty = - AvaloniaProperty.RegisterAttached("OriginTemplate", typeof(RecyclePool)); + internal static readonly AttachedProperty OriginTemplateProperty = + AvaloniaProperty.RegisterAttached("OriginTemplate"); + + internal static readonly AttachedProperty ReuseKeyProperty = + AvaloniaProperty.RegisterAttached("ReuseKey", string.Empty); private static ConditionalWeakTable s_pools = new ConditionalWeakTable(); private readonly Dictionary> _elements = new Dictionary>(); @@ -77,6 +80,9 @@ namespace Avalonia.Controls return null; } + internal string GetReuseKey(IControl element) => element.GetValue(ReuseKeyProperty); + internal void SetReuseKey(IControl element, string value) => element.SetValue(ReuseKeyProperty, value); + private IPanel EnsureOwnerIsPanelOrNull(IControl owner) { if (owner is IPanel panel) diff --git a/src/Avalonia.Controls/Repeater/RecyclingElementFactory.cs b/src/Avalonia.Controls/Repeater/RecyclingElementFactory.cs new file mode 100644 index 0000000000..9503239e34 --- /dev/null +++ b/src/Avalonia.Controls/Repeater/RecyclingElementFactory.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls.Templates; + +#nullable enable + +namespace Avalonia.Controls +{ + public class SelectTemplateEventArgs : EventArgs + { + public string? TemplateKey { get; set; } + public object? DataContext { get; internal set; } + public IControl? Owner { get; internal set; } + } + + public class RecyclingElementFactory : ElementFactory + { + private RecyclePool? _recyclePool; + private IDictionary? _templates; + private SelectTemplateEventArgs? _args; + + public RecyclingElementFactory() + { + Templates = new Dictionary(); + } + + public RecyclePool RecyclePool + { + get => _recyclePool ??= new RecyclePool(); + set => _recyclePool = value ?? throw new ArgumentNullException(nameof(value)); + } + + public IDictionary Templates + { + get => _templates ??= new Dictionary(); + set => _templates = value ?? throw new ArgumentNullException(nameof(value)); + } + + public event EventHandler? SelectTemplateKey; + + protected override IControl GetElementCore(ElementFactoryGetArgs args) + { + if (_templates == null || _templates.Count == 0) + { + throw new InvalidOperationException("Templates cannot be empty."); + } + + var templateKey = Templates.Count == 1 ? + Templates.First().Key : + OnSelectTemplateKeyCore(args.Data, args.Parent); + + if (string.IsNullOrEmpty(templateKey)) + { + // Note: We could allow null/whitespace, which would work as long as + // the recycle pool is not shared. in order to make this work in all cases + // currently we validate that a valid template key is provided. + throw new InvalidOperationException("Template key cannot be null or empty."); + } + + // Get an element from the Recycle Pool or create one + var element = RecyclePool.TryGetElement(templateKey, args.Parent); + + if (element is null) + { + // No need to call HasKey if there is only one template. + if (Templates.Count > 1 && !Templates.ContainsKey(templateKey)) + { + var message = $"No templates of key '{templateKey}' were found in the templates collection."; + throw new InvalidOperationException(message); + } + + var dataTemplate = Templates[templateKey]; + element = dataTemplate.Build(args.Data); + + // Associate ReuseKey with element + RecyclePool.SetReuseKey(element, templateKey); + } + + return element; + } + + protected override void RecycleElementCore(ElementFactoryRecycleArgs args) + { + var element = args.Element; + var key = RecyclePool.GetReuseKey(element); + RecyclePool.PutElement(element, key, args.Parent); + } + + protected virtual string OnSelectTemplateKeyCore(object dataContext, IControl owner) + { + if (SelectTemplateKey is object) + { + _args ??= new SelectTemplateEventArgs(); + _args.TemplateKey = null; + _args.DataContext = dataContext; + _args.Owner = owner; + + try + { + SelectTemplateKey(this, _args); + } + finally + { + _args.DataContext = null; + _args.Owner = null; + } + } + + if (string.IsNullOrEmpty(_args?.TemplateKey)) + { + throw new InvalidOperationException( + "Please provide a valid template identifier in the handler for the SelectTemplateKey event."); + } + + return _args!.TemplateKey!; + } + } +} diff --git a/src/Avalonia.Controls/Repeater/ViewManager.cs b/src/Avalonia.Controls/Repeater/ViewManager.cs index eff51804b9..416b1e2824 100644 --- a/src/Avalonia.Controls/Repeater/ViewManager.cs +++ b/src/Avalonia.Controls/Repeater/ViewManager.cs @@ -6,11 +6,9 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; -using System.Linq; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; -using Avalonia.Layout; using Avalonia.Logging; using Avalonia.VisualTree; @@ -26,6 +24,8 @@ namespace Avalonia.Controls private readonly UniqueIdElementPool _resetPool; private IControl _lastFocusedElement; private bool _isDataSourceStableResetPending; + private ElementFactoryGetArgs _elementFactoryGetArgs; + private ElementFactoryRecycleArgs _elementFactoryRecycleArgs; private int _firstRealizedElementIndexHeldByLayout = FirstRealizedElementIndexDefault; private int _lastRealizedElementIndexHeldByLayout = LastRealizedElementIndexDefault; private bool _eventsSubscribed; @@ -134,7 +134,14 @@ namespace Avalonia.Controls if (_owner.ItemTemplateShim != null) { - _owner.ItemTemplateShim.RecycleElement(_owner, element); + var context = _elementFactoryRecycleArgs ??= new ElementFactoryRecycleArgs(); + context.Element = element; + context.Parent = _owner; + + _owner.ItemTemplateShim.RecycleElement(context); + + context.Element = null; + context.Parent = null; } else { @@ -579,7 +586,7 @@ namespace Avalonia.Controls var data = _owner.ItemsSourceView.GetAt(index); var providedElementFactory = _owner.ItemTemplateShim; - ItemTemplateWrapper GetElementFactory() + IElementFactory GetElementFactory() { if (providedElementFactory == null) { @@ -602,7 +609,20 @@ namespace Avalonia.Controls } var elementFactory = GetElementFactory(); - return elementFactory.GetElement(_owner, data); + var args = _elementFactoryGetArgs ??= new ElementFactoryGetArgs(); + + try + { + args.Data = data; + args.Parent = _owner; + args.Index = index; + return elementFactory.GetElement(args); + } + finally + { + args.Data = null; + args.Parent = null; + } } var element = GetElement(); @@ -732,6 +752,7 @@ namespace Avalonia.Controls { _owner.GotFocus += OnFocusChanged; _owner.LostFocus += OnFocusChanged; + _eventsSubscribed = true; } } diff --git a/src/Avalonia.Controls/Templates/FuncDataTemplate.cs b/src/Avalonia.Controls/Templates/FuncDataTemplate.cs index d454a29021..1afd86a11e 100644 --- a/src/Avalonia.Controls/Templates/FuncDataTemplate.cs +++ b/src/Avalonia.Controls/Templates/FuncDataTemplate.cs @@ -6,7 +6,7 @@ namespace Avalonia.Controls.Templates /// /// Builds a control for a piece of data. /// - public class FuncDataTemplate : FuncTemplate, IDataTemplate + public class FuncDataTemplate : FuncTemplate, IRecyclingDataTemplate { /// /// The default data template used in the case where no matching data template is found. @@ -30,10 +30,8 @@ namespace Avalonia.Controls.Templates }, true); - /// - /// The implementation of the method. - /// private readonly Func _match; + private readonly bool _supportsRecycling; /// /// Initializes a new instance of the class. @@ -70,12 +68,9 @@ namespace Avalonia.Controls.Templates Contract.Requires(match != null); _match = match; - SupportsRecycling = supportsRecycling; + _supportsRecycling = supportsRecycling; } - /// - public bool SupportsRecycling { get; } - /// /// Checks to see if this data template matches the specified data. /// @@ -88,6 +83,24 @@ namespace Avalonia.Controls.Templates return _match(data); } + /// + /// Creates or recycles a control to display the specified data. + /// + /// The data to display. + /// An optional control to recycle. + /// + /// The control if supplied and applicable to + /// , otherwise a new control. + /// + /// + /// The caller should ensure that any control passed to + /// originated from the same data template. + /// + public IControl Build(object data, IControl existing) + { + return _supportsRecycling && existing is object ? existing : Build(data); + } + /// /// Determines of an object is of the specified type. /// diff --git a/src/Avalonia.Controls/Templates/FuncTemplate`2.cs b/src/Avalonia.Controls/Templates/FuncTemplate`2.cs index d08616b968..cd0e3ad603 100644 --- a/src/Avalonia.Controls/Templates/FuncTemplate`2.cs +++ b/src/Avalonia.Controls/Templates/FuncTemplate`2.cs @@ -1,5 +1,7 @@ using System; +#nullable enable + namespace Avalonia.Controls.Templates { /// @@ -18,9 +20,7 @@ namespace Avalonia.Controls.Templates /// The function used to create the control. public FuncTemplate(Func func) { - Contract.Requires(func != null); - - _func = func; + _func = func ?? throw new ArgumentNullException(nameof(func)); } /// diff --git a/src/Avalonia.Controls/Templates/IDataTemplate.cs b/src/Avalonia.Controls/Templates/IDataTemplate.cs index cfde029eb8..0368748a0b 100644 --- a/src/Avalonia.Controls/Templates/IDataTemplate.cs +++ b/src/Avalonia.Controls/Templates/IDataTemplate.cs @@ -1,3 +1,7 @@ +using System; + +#nullable enable + namespace Avalonia.Controls.Templates { /// @@ -5,12 +9,6 @@ namespace Avalonia.Controls.Templates /// public interface IDataTemplate : ITemplate { - /// - /// Gets a value indicating whether the data template supports recycling of the generated - /// control. - /// - bool SupportsRecycling { get; } - /// /// Checks to see if this data template matches the specified data. /// @@ -20,4 +18,4 @@ namespace Avalonia.Controls.Templates /// bool Match(object data); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Templates/IRecyclingDataTemplate.cs b/src/Avalonia.Controls/Templates/IRecyclingDataTemplate.cs new file mode 100644 index 0000000000..25956a9c9a --- /dev/null +++ b/src/Avalonia.Controls/Templates/IRecyclingDataTemplate.cs @@ -0,0 +1,25 @@ +#nullable enable + +namespace Avalonia.Controls.Templates +{ + /// + /// An that supports recycling existing elements. + /// + public interface IRecyclingDataTemplate : IDataTemplate + { + /// + /// Creates or recycles a control to display the specified data. + /// + /// The data to display. + /// An optional control to recycle. + /// + /// The control if supplied and applicable to + /// , otherwise a new control. + /// + /// + /// The caller should ensure that any control passed to + /// originated from the same data template. + /// + IControl Build(object data, IControl? existing); + } +} diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 394699ce64..87a674fabd 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -134,7 +134,7 @@ namespace Avalonia.Controls { if (acceptsReturn) { - return wrapping == TextWrapping.NoWrap ? + return wrapping != TextWrapping.Wrap ? ScrollBarVisibility.Auto : ScrollBarVisibility.Disabled; } diff --git a/src/Avalonia.Controls/TickBar.cs b/src/Avalonia.Controls/TickBar.cs index 22145d8742..6ea5277a55 100644 --- a/src/Avalonia.Controls/TickBar.cs +++ b/src/Avalonia.Controls/TickBar.cs @@ -39,11 +39,15 @@ namespace Avalonia.Controls { static TickBar() { - AffectsRender(ReservedSpaceProperty, + AffectsRender(FillProperty, + IsDirectionReversedProperty, + ReservedSpaceProperty, MaximumProperty, MinimumProperty, OrientationProperty, - TickFrequencyProperty); + PlacementProperty, + TickFrequencyProperty, + TicksProperty); } public TickBar() : base() @@ -137,7 +141,7 @@ namespace Avalonia.Controls /// /// The Ticks property contains collection of value of type Double which /// are the logical positions use to draw the ticks. - /// The property value is a . + /// The property value is a . /// public AvaloniaList Ticks { @@ -169,7 +173,6 @@ namespace Avalonia.Controls public static readonly StyledProperty PlacementProperty = AvaloniaProperty.Register(nameof(Placement), 0d); - /// /// Placement property specified how the Tick will be placed. /// This property affects the way ticks are drawn. @@ -189,7 +192,7 @@ namespace Avalonia.Controls /// /// TickBar will use ReservedSpaceProperty for left and right spacing (for horizontal orientation) or - /// tob and bottom spacing (for vertical orienation). + /// top and bottom spacing (for vertical orienation). /// The space on both sides of TickBar is half of specified ReservedSpace. /// This property has type of . /// @@ -201,7 +204,7 @@ namespace Avalonia.Controls /// /// Draw ticks. - /// Ticks can be draw in 8 diffrent ways depends on Placment property and IsDirectionReversed property. + /// Ticks can be draw in 8 different ways depends on Placement property and IsDirectionReversed property. /// /// This function also draw selection-tick(s) if IsSelectionRangeEnabled is 'true' and /// SelectionStart and SelectionEnd are valid. @@ -211,9 +214,7 @@ namespace Avalonia.Controls /// /// The secondary ticks (all other ticks, including selection-tics) height will be 75% of TickBar's render size. /// - /// Brush that use to fill ticks is specified by Shape.Fill property. - /// - /// Pen that use to draw ticks is specified by Shape.Pen property. + /// Brush that use to fill ticks is specified by Fill property. /// public override void Render(DrawingContext dc) { @@ -222,7 +223,6 @@ namespace Avalonia.Controls var tickLen = 0.0d; // Height for Primary Tick (for Mininum and Maximum value) var tickLen2 = 0.0d; // Height for Secondary Tick var logicalToPhysical = 1.0; - var progression = 1.0d; var startPoint = new Point(); var endPoint = new Point(); var rSpace = Orientation == Orientation.Horizontal ? ReservedSpace.Width : ReservedSpace.Height; @@ -242,7 +242,6 @@ namespace Avalonia.Controls startPoint = new Point(halfReservedSpace, size.Height); endPoint = new Point(halfReservedSpace + size.Width, size.Height); logicalToPhysical = size.Width / range; - progression = 1; break; case TickBarPlacement.Bottom: @@ -255,7 +254,6 @@ namespace Avalonia.Controls startPoint = new Point(halfReservedSpace, 0d); endPoint = new Point(halfReservedSpace + size.Width, 0d); logicalToPhysical = size.Width / range; - progression = 1; break; case TickBarPlacement.Left: @@ -269,7 +267,6 @@ namespace Avalonia.Controls startPoint = new Point(size.Width, size.Height + halfReservedSpace); endPoint = new Point(size.Width, halfReservedSpace); logicalToPhysical = size.Height / range * -1; - progression = -1; break; case TickBarPlacement.Right: @@ -282,7 +279,6 @@ namespace Avalonia.Controls startPoint = new Point(0d, size.Height + halfReservedSpace); endPoint = new Point(0d, halfReservedSpace); logicalToPhysical = size.Height / range * -1; - progression = -1; break; }; @@ -291,7 +287,6 @@ namespace Avalonia.Controls // Invert direciton of the ticks if (IsDirectionReversed) { - progression *= -progression; logicalToPhysical *= -1; // swap startPoint & endPoint diff --git a/src/Avalonia.Controls/ToolTipService.cs b/src/Avalonia.Controls/ToolTipService.cs index d90729e8a5..569697304f 100644 --- a/src/Avalonia.Controls/ToolTipService.cs +++ b/src/Avalonia.Controls/ToolTipService.cs @@ -28,14 +28,22 @@ namespace Avalonia.Controls { control.PointerEnter -= ControlPointerEnter; control.PointerLeave -= ControlPointerLeave; + control.DetachedFromVisualTree -= ControlDetaching; } if (e.NewValue != null) { control.PointerEnter += ControlPointerEnter; control.PointerLeave += ControlPointerLeave; + control.DetachedFromVisualTree += ControlDetaching; } } + + private void ControlDetaching(object sender, VisualTreeAttachmentEventArgs e) + { + var control = (Control)sender; + Close(control); + } /// /// Called when the pointer enters a control with an attached tooltip. diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 611f0c9290..3d24f60463 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -280,10 +280,10 @@ namespace Avalonia.Controls } /// - double ILayoutRoot.LayoutScaling => PlatformImpl?.Scaling ?? 1; + double ILayoutRoot.LayoutScaling => PlatformImpl?.RenderScaling ?? 1; /// - double IRenderRoot.RenderScaling => PlatformImpl?.Scaling ?? 1; + double IRenderRoot.RenderScaling => PlatformImpl?.RenderScaling ?? 1; IStyleHost IStyleHost.StylingParent => _globalStyles; diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 18d8c89f49..d8e7f3a387 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -6,6 +6,7 @@ using System.Reactive.Linq; using System.Threading.Tasks; using Avalonia.Controls.Chrome; using Avalonia.Controls.Platform; +using Avalonia.Controls.Primitives; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; @@ -70,8 +71,7 @@ namespace Avalonia.Controls /// public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot { - private readonly List<(Window child, bool isDialog)> _children = new List<(Window, bool)>(); - private TitleBar _managedTitleBar; + private readonly List<(Window child, bool isDialog)> _children = new List<(Window, bool)>(); private bool _isExtendedIntoWindowDecorations; private Thickness _windowDecorationMargin; private Thickness _offScreenMargin; @@ -552,20 +552,6 @@ namespace Avalonia.Controls IsExtendedIntoWindowDecorations = isExtended; WindowDecorationMargin = PlatformImpl.ExtendedMargins; OffScreenMargin = PlatformImpl.OffScreenMargin; - - if (PlatformImpl.NeedsManagedDecorations) - { - if (_managedTitleBar == null) - { - _managedTitleBar = new TitleBar(this); - _managedTitleBar.Attach(); - } - } - else - { - _managedTitleBar?.Detach(); - _managedTitleBar = null; - } } /// @@ -818,7 +804,7 @@ namespace Avalonia.Controls private void SetWindowStartupLocation(IWindowBaseImpl owner = null) { - var scaling = owner?.Scaling ?? PlatformImpl?.Scaling ?? 1; + var scaling = owner?.DesktopScaling ?? PlatformImpl?.DesktopScaling ?? 1; // TODO: We really need non-client size here. var rect = new PixelRect( diff --git a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs index f3bb0edce5..59862da230 100644 --- a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs +++ b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs @@ -18,10 +18,11 @@ namespace Avalonia.DesignerSupport Control control; using (PlatformManager.DesignerMode()) { - var loader = new AvaloniaXamlLoader() {IsDesignMode = true}; + var loader = AvaloniaLocator.Current.GetService(); var stream = new MemoryStream(Encoding.UTF8.GetBytes(xaml)); - + if (loader == null) + throw new XamlLoadException("Runtime XAML loader is not registered"); Uri baseUri = null; if (assemblyPath != null) @@ -34,7 +35,7 @@ namespace Avalonia.DesignerSupport } var localAsm = assemblyPath != null ? Assembly.LoadFile(Path.GetFullPath(assemblyPath)) : null; - var loaded = loader.Load(stream, localAsm, null, baseUri); + var loaded = loader.Load(stream, localAsm, null, baseUri, true); var style = loaded as IStyle; if (style != null) { diff --git a/src/Avalonia.DesignerSupport/Remote/FileWatcherTransport.cs b/src/Avalonia.DesignerSupport/Remote/FileWatcherTransport.cs index 0cb71dd217..0448a5c05d 100644 --- a/src/Avalonia.DesignerSupport/Remote/FileWatcherTransport.cs +++ b/src/Avalonia.DesignerSupport/Remote/FileWatcherTransport.cs @@ -9,12 +9,14 @@ namespace Avalonia.DesignerSupport.Remote { class FileWatcherTransport : IAvaloniaRemoteTransportConnection, ITransportWithEnforcedMethod { + private readonly string _appPath; private string _path; private string _lastContents; private bool _disposed; - public FileWatcherTransport(Uri file) + public FileWatcherTransport(Uri file, string appPath) { + _appPath = appPath; _path = file.LocalPath; } @@ -73,7 +75,11 @@ namespace Avalonia.DesignerSupport.Remote { Console.WriteLine("Triggering XAML update"); _lastContents = data; - _onMessage?.Invoke(this, new UpdateXamlMessage { Xaml = data }); + _onMessage?.Invoke(this, new UpdateXamlMessage + { + Xaml = data, + AssemblyPath = _appPath + }); } await Task.Delay(100); diff --git a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json index 87536c670f..028027a974 100644 --- a/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json +++ b/src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json @@ -3564,12 +3564,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3584,17 +3586,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -3711,7 +3716,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -3723,6 +3729,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -3737,6 +3744,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -3744,12 +3752,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -3768,6 +3778,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -3848,7 +3859,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -3860,6 +3872,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -3981,6 +3994,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index dce24df9d9..25c26be91e 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -36,6 +36,7 @@ namespace Avalonia.DesignerSupport.Remote { } + public double DesktopScaling => 1.0; public PixelPoint Position { get; set; } public Action PositionChanged { get; set; } public Action Deactivated { get; set; } diff --git a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs index e61fe82c41..764fc7b332 100644 --- a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs +++ b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs @@ -112,8 +112,9 @@ namespace Avalonia.DesignerSupport.Remote return rv; } - static IAvaloniaRemoteTransportConnection CreateTransport(Uri transport) + static IAvaloniaRemoteTransportConnection CreateTransport(CommandLineArgs args) { + var transport = args.Transport; if (transport.Scheme == "tcp-bson") { return new BsonTcpTransport().Connect(IPAddress.Parse(transport.Host), transport.Port).Result; @@ -121,7 +122,7 @@ namespace Avalonia.DesignerSupport.Remote if (transport.Scheme == "file") { - return new FileWatcherTransport(transport); + return new FileWatcherTransport(transport, args.AppPath); } PrintUsage(); return null; @@ -160,7 +161,7 @@ namespace Avalonia.DesignerSupport.Remote public static void Main(string[] cmdline) { var args = ParseCommandLineArgs(cmdline); - var transport = CreateTransport(args.Transport); + var transport = CreateTransport(args); if (transport is ITransportWithEnforcedMethod enforcedMethod) args.Method = enforcedMethod.PreviewerMethod; var asm = Assembly.LoadFile(System.IO.Path.GetFullPath(args.AppPath)); diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 168cdbc03f..f377b1bcd1 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -21,7 +21,8 @@ namespace Avalonia.DesignerSupport.Remote public IPlatformHandle Handle { get; } public Size MaxAutoSizeHint { get; } public Size ClientSize { get; } - public double Scaling { get; } = 1.0; + public double RenderScaling { get; } = 1.0; + public double DesktopScaling => 1.0; public IEnumerable Surfaces { get; } public Action Input { get; set; } public Action Paint { get; set; } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs index c06fbec801..be3564e781 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs +++ b/src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs @@ -7,8 +7,6 @@ namespace Avalonia.Diagnostics { internal class ViewLocator : IDataTemplate { - public bool SupportsRecycling => false; - public IControl Build(object data) { var name = data.GetType().FullName.Replace("ViewModel", "View"); diff --git a/src/Avalonia.FreeDesktop/DBusHelper.cs b/src/Avalonia.FreeDesktop/DBusHelper.cs index b445f86613..91c4c28995 100644 --- a/src/Avalonia.FreeDesktop/DBusHelper.cs +++ b/src/Avalonia.FreeDesktop/DBusHelper.cs @@ -43,7 +43,7 @@ namespace Avalonia.FreeDesktop public void Initialized() { lock (_lock) - _ctx = new AvaloniaSynchronizationContext(); + _ctx = new AvaloniaSynchronizationContext(null); } } public static Connection Connection { get; private set; } diff --git a/src/Avalonia.Headless/HeadlessWindowImpl.cs b/src/Avalonia.Headless/HeadlessWindowImpl.cs index 5bd46b6714..8f4fa5e304 100644 --- a/src/Avalonia.Headless/HeadlessWindowImpl.cs +++ b/src/Avalonia.Headless/HeadlessWindowImpl.cs @@ -41,7 +41,8 @@ namespace Avalonia.Headless } public Size ClientSize { get; set; } - public double Scaling { get; } = 1; + public double RenderScaling { get; } = 1; + public double DesktopScaling => RenderScaling; public IEnumerable Surfaces { get; } public Action Input { get; set; } public Action Paint { get; set; } @@ -62,9 +63,9 @@ namespace Avalonia.Headless public IInputRoot InputRoot { get; set; } - public Point PointToClient(PixelPoint point) => point.ToPoint(Scaling); + public Point PointToClient(PixelPoint point) => point.ToPoint(RenderScaling); - public PixelPoint PointToScreen(Point point) => PixelPoint.FromPoint(point, Scaling); + public PixelPoint PointToScreen(Point point) => PixelPoint.FromPoint(point, RenderScaling); public void SetCursor(IPlatformHandle cursor) { @@ -201,7 +202,7 @@ namespace Avalonia.Headless public ILockedFramebuffer Lock() { - var bmp = new WriteableBitmap(PixelSize.FromSize(ClientSize, Scaling), new Vector(96, 96) * Scaling); + var bmp = new WriteableBitmap(PixelSize.FromSize(ClientSize, RenderScaling), new Vector(96, 96) * RenderScaling); var fb = bmp.Lock(); return new FramebufferProxy(fb, () => { diff --git a/src/Avalonia.Native/OsxManagedPopupPositionerPopupImplHelper.cs b/src/Avalonia.Native/OsxManagedPopupPositionerPopupImplHelper.cs deleted file mode 100644 index 8aa9b1a122..0000000000 --- a/src/Avalonia.Native/OsxManagedPopupPositionerPopupImplHelper.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Avalonia.Controls.Primitives.PopupPositioning; -using Avalonia.Platform; - -namespace Avalonia.Native -{ - class OsxManagedPopupPositionerPopupImplHelper : ManagedPopupPositionerPopupImplHelper - { - public OsxManagedPopupPositionerPopupImplHelper(IWindowBaseImpl parent, MoveResizeDelegate moveResize) : base(parent, moveResize) - { - - } - - public override double Scaling => 1; - } -} diff --git a/src/Avalonia.Native/OsxUnicodeKeys.cs b/src/Avalonia.Native/OsxUnicodeKeys.cs index b4056c8cd8..fd9acfa714 100644 --- a/src/Avalonia.Native/OsxUnicodeKeys.cs +++ b/src/Avalonia.Native/OsxUnicodeKeys.cs @@ -140,6 +140,11 @@ namespace Avalonia.Native.Interop } else { + if (key >= Key.D0 && key <= Key.D9) + { + return key.ToString().Replace("D", ""); + } + return key.ToString().ToLower(); } } diff --git a/src/Avalonia.Native/PopupImpl.cs b/src/Avalonia.Native/PopupImpl.cs index b0da5fdc43..2d246e08d2 100644 --- a/src/Avalonia.Native/PopupImpl.cs +++ b/src/Avalonia.Native/PopupImpl.cs @@ -26,7 +26,7 @@ namespace Avalonia.Native var context = _opts.UseGpu ? glFeature?.DeferredContext : null; Init(factory.CreatePopup(e, context?.Context), factory.CreateScreens(), context); } - PopupPositioner = new ManagedPopupPositioner(new OsxManagedPopupPositionerPopupImplHelper(parent, MoveResize)); + PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, MoveResize)); } private void MoveResize(PixelPoint position, Size size, double scaling) diff --git a/src/Avalonia.Native/WindowImplBase.cs b/src/Avalonia.Native/WindowImplBase.cs index f916e95d7c..42eecc36ea 100644 --- a/src/Avalonia.Native/WindowImplBase.cs +++ b/src/Avalonia.Native/WindowImplBase.cs @@ -81,7 +81,7 @@ namespace Avalonia.Native _glSurface = new GlPlatformSurface(window, _glContext); Screen = new ScreenImpl(screens); _savedLogicalSize = ClientSize; - _savedScaling = Scaling; + _savedScaling = RenderScaling; _nativeControlHost = new NativeControlHostImpl(_native.CreateNativeControlHost()); var monitor = Screen.AllScreens.OrderBy(x => x.PixelDensity) @@ -369,7 +369,9 @@ namespace Avalonia.Native _native.SetTopMost(value); } - public double Scaling => _native?.GetScaling() ?? 1; + public double RenderScaling => _native?.GetScaling() ?? 1; + + public double DesktopScaling => 1; public Action Deactivated { get; set; } public Action Activated { get; set; } @@ -432,7 +434,7 @@ namespace Avalonia.Native TransparencyLevel = transparencyLevel; - _native.SetBlurEnabled(TransparencyLevel >= WindowTransparencyLevel.Blur); + _native?.SetBlurEnabled(TransparencyLevel >= WindowTransparencyLevel.Blur); TransparencyLevelChanged?.Invoke(TransparencyLevel); } } diff --git a/src/Avalonia.Themes.Default/TextBox.xaml b/src/Avalonia.Themes.Default/TextBox.xaml index 4fb3653e89..6a746dda30 100644 --- a/src/Avalonia.Themes.Default/TextBox.xaml +++ b/src/Avalonia.Themes.Default/TextBox.xaml @@ -40,6 +40,8 @@ - + diff --git a/src/Avalonia.Themes.Default/ToggleSwitch.xaml b/src/Avalonia.Themes.Default/ToggleSwitch.xaml index 893d64f505..ded121f5f6 100644 --- a/src/Avalonia.Themes.Default/ToggleSwitch.xaml +++ b/src/Avalonia.Themes.Default/ToggleSwitch.xaml @@ -43,7 +43,8 @@ - + + diff --git a/src/Avalonia.Themes.Default/Window.xaml b/src/Avalonia.Themes.Default/Window.xaml index 739887fb35..1856eec206 100644 --- a/src/Avalonia.Themes.Default/Window.xaml +++ b/src/Avalonia.Themes.Default/Window.xaml @@ -1,23 +1,25 @@ @@ -145,6 +148,8 @@ @@ -155,8 +160,7 @@ - + + + @@ -212,14 +221,15 @@ - - - diff --git a/src/Avalonia.Themes.Fluent/NumericUpDown.xaml b/src/Avalonia.Themes.Fluent/NumericUpDown.xaml index d1610b343c..cc5e2234f8 100644 --- a/src/Avalonia.Themes.Fluent/NumericUpDown.xaml +++ b/src/Avalonia.Themes.Fluent/NumericUpDown.xaml @@ -43,7 +43,7 @@ - - - + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - + - - - - - - + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/TitleBar.xaml b/src/Avalonia.Themes.Fluent/TitleBar.xaml index 45798d3fa1..4dba5b4ba4 100644 --- a/src/Avalonia.Themes.Fluent/TitleBar.xaml +++ b/src/Avalonia.Themes.Fluent/TitleBar.xaml @@ -15,7 +15,7 @@ - + diff --git a/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml b/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml index e7f8fb1641..4309edefe3 100644 --- a/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml +++ b/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml @@ -43,7 +43,8 @@ - + + diff --git a/src/Avalonia.Themes.Fluent/Window.xaml b/src/Avalonia.Themes.Fluent/Window.xaml index 794f516f71..1856eec206 100644 --- a/src/Avalonia.Themes.Fluent/Window.xaml +++ b/src/Avalonia.Themes.Fluent/Window.xaml @@ -8,16 +8,18 @@ - - - - - + + + + + + + diff --git a/src/Avalonia.Visuals/Media/Color.cs b/src/Avalonia.Visuals/Media/Color.cs index 052ee5e1b7..16b4f90d57 100644 --- a/src/Avalonia.Visuals/Media/Color.cs +++ b/src/Avalonia.Visuals/Media/Color.cs @@ -89,6 +89,11 @@ namespace Avalonia.Media /// The . public static Color Parse(string s) { + if (s is null) + { + throw new ArgumentNullException(nameof(s)); + } + if (TryParse(s, out Color color)) { return color; @@ -120,14 +125,16 @@ namespace Avalonia.Media /// The status of the operation. public static bool TryParse(string s, out Color color) { - if (s == null) + color = default; + + if (s is null) { - throw new ArgumentNullException(nameof(s)); + return false; } if (s.Length == 0) { - throw new FormatException(); + return false; } if (s[0] == '#' && TryParseInternal(s.AsSpan(), out color)) @@ -144,8 +151,6 @@ namespace Avalonia.Media return true; } - color = default; - return false; } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/GenericTextParagraphProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/GenericTextParagraphProperties.cs index c4302aecec..8e7d934bca 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/GenericTextParagraphProperties.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/GenericTextParagraphProperties.cs @@ -4,14 +4,12 @@ { private TextAlignment _textAlignment; private TextWrapping _textWrapping; - private TextTrimming _textTrimming; private double _lineHeight; public GenericTextParagraphProperties( TextRunProperties defaultTextRunProperties, TextAlignment textAlignment = TextAlignment.Left, - TextWrapping textWrapping = TextWrapping.WrapWithOverflow, - TextTrimming textTrimming = TextTrimming.None, + TextWrapping textWrapping = TextWrapping.NoWrap, double lineHeight = 0) { DefaultTextRunProperties = defaultTextRunProperties; @@ -20,8 +18,6 @@ _textWrapping = textWrapping; - _textTrimming = textTrimming; - _lineHeight = lineHeight; } @@ -31,8 +27,6 @@ public override TextWrapping TextWrapping => _textWrapping; - public override TextTrimming TextTrimming => _textTrimming; - public override double LineHeight => _lineHeight; /// @@ -50,13 +44,6 @@ { _textWrapping = textWrapping; } - /// - /// Set text trimming - /// - internal void SetTextTrimming(TextTrimming textTrimming) - { - _textTrimming = textTrimming; - } /// /// Set line height diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs index b71fe5bc3c..9e67a03f45 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs @@ -1,5 +1,4 @@ -using Avalonia.Media.TextFormatting.Unicode; -using Avalonia.Utilities; +using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs new file mode 100644 index 0000000000..ffd65423a3 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs @@ -0,0 +1,23 @@ +namespace Avalonia.Media.TextFormatting +{ + /// + /// Properties of text collapsing + /// + public abstract class TextCollapsingProperties + { + /// + /// Gets the width in which the collapsible range is constrained to + /// + public abstract double Width { get; } + + /// + /// Gets the text run that is used as collapsing symbol + /// + public abstract TextRun Symbol { get; } + + /// + /// Gets the style of collapsing + /// + public abstract TextCollapsingStyle Style { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingStyle.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingStyle.cs new file mode 100644 index 0000000000..1523cc4d9a --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingStyle.cs @@ -0,0 +1,18 @@ +namespace Avalonia.Media.TextFormatting +{ + /// + /// Text collapsing style + /// + public enum TextCollapsingStyle + { + /// + /// Collapse trailing characters + /// + TrailingCharacter, + + /// + /// Collapse trailing words + /// + TrailingWord, + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs index 3ad23f3504..9318fcc68e 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs @@ -1,52 +1,194 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Avalonia.Media.TextFormatting.Unicode; -using Avalonia.Platform; -using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { internal class TextFormatterImpl : TextFormatter { - private static readonly ReadOnlySlice s_ellipsis = new ReadOnlySlice(new[] { '\u2026' }); - /// public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties, TextLineBreak previousLineBreak = null) { - var textTrimming = paragraphProperties.TextTrimming; var textWrapping = paragraphProperties.TextWrapping; - TextLine textLine = null; var textRuns = FetchTextRuns(textSource, firstTextSourceIndex, previousLineBreak, out var nextLineBreak); var textRange = GetTextRange(textRuns); - if (textTrimming != TextTrimming.None) + TextLine textLine; + + switch (textWrapping) + { + case TextWrapping.NoWrap: + { + var textLineMetrics = + TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties); + + textLine = new TextLineImpl(textRuns, textLineMetrics, nextLineBreak); + break; + } + case TextWrapping.WrapWithOverflow: + case TextWrapping.Wrap: + { + textLine = PerformTextWrapping(textRuns, textRange, paragraphWidth, paragraphProperties); + break; + } + default: + throw new ArgumentOutOfRangeException(); + } + + return textLine; + } + + /// + /// Measures the number of characters that fits into available width. + /// + /// The text run. + /// The available width. + /// + internal static int MeasureCharacters(ShapedTextCharacters textCharacters, double availableWidth) + { + var glyphRun = textCharacters.GlyphRun; + + if (glyphRun.Bounds.Width < availableWidth) { - textLine = PerformTextTrimming(textRuns, textRange, paragraphWidth, paragraphProperties); + return glyphRun.Characters.Length; + } + + var glyphCount = 0; + + var currentWidth = 0.0; + + if (glyphRun.GlyphAdvances.IsEmpty) + { + var glyphTypeface = glyphRun.GlyphTypeface; + + for (var i = 0; i < glyphRun.GlyphClusters.Length; i++) + { + var glyph = glyphRun.GlyphIndices[i]; + + var advance = glyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale; + + if (currentWidth + advance > availableWidth) + { + break; + } + + currentWidth += advance; + + glyphCount++; + } } else { - switch (textWrapping) + foreach (var advance in glyphRun.GlyphAdvances) { - case TextWrapping.NoWrap: - { - var textLineMetrics = - TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties); + if (currentWidth + advance > availableWidth) + { + break; + } - textLine = new TextLineImpl(textRuns, textLineMetrics, nextLineBreak); - break; + currentWidth += advance; + + glyphCount++; + } + } + + if (glyphCount == glyphRun.GlyphIndices.Length) + { + return glyphRun.Characters.Length; + } + + if (glyphRun.GlyphClusters.IsEmpty) + { + return glyphCount; + } + + var firstCluster = glyphRun.GlyphClusters[0]; + + var lastCluster = glyphRun.GlyphClusters[glyphCount]; + + return lastCluster - firstCluster; + } + + /// + /// Split a sequence of runs into two segments at specified length. + /// + /// The text run's. + /// The length to split at. + /// The split text runs. + internal static SplitTextRunsResult SplitTextRuns(IReadOnlyList textRuns, int length) + { + var currentLength = 0; + + for (var i = 0; i < textRuns.Count; i++) + { + var currentRun = textRuns[i]; + + if (currentLength + currentRun.GlyphRun.Characters.Length < length) + { + currentLength += currentRun.GlyphRun.Characters.Length; + continue; + } + + var firstCount = currentRun.GlyphRun.Characters.Length >= 1 ? i + 1 : i; + + var first = new ShapedTextCharacters[firstCount]; + + if (firstCount > 1) + { + for (var j = 0; j < i; j++) + { + first[j] = textRuns[j]; + } + } + + var secondCount = textRuns.Count - firstCount; + + if (currentLength + currentRun.GlyphRun.Characters.Length == length) + { + var second = new ShapedTextCharacters[secondCount]; + + var offset = currentRun.GlyphRun.Characters.Length > 1 ? 1 : 0; + + if (secondCount > 0) + { + for (var j = 0; j < secondCount; j++) + { + second[j] = textRuns[i + j + offset]; } - case TextWrapping.WrapWithOverflow: - case TextWrapping.Wrap: + } + + first[i] = currentRun; + + return new SplitTextRunsResult(first, second); + } + else + { + secondCount++; + + var second = new ShapedTextCharacters[secondCount]; + + if (secondCount > 0) + { + for (var j = 1; j < secondCount; j++) { - textLine = PerformTextWrapping(textRuns, textRange, paragraphWidth, paragraphProperties); - break; + second[j] = textRuns[i + j]; } + } + + var split = currentRun.Split(length - currentLength); + + first[i] = split.First; + + second[0] = split.Second; + + return new SplitTextRunsResult(first, second); } } - return textLine; + return new SplitTextRunsResult(textRuns, null); } /// @@ -174,87 +316,6 @@ namespace Avalonia.Media.TextFormatting return false; } - /// - /// Performs text trimming and returns a trimmed line. - /// - /// The text runs to perform the trimming on. - /// The text range that is covered by the text runs. - /// A value that specifies the width of the paragraph that the line fills. - /// A value that represents paragraph properties, - /// such as TextWrapping, TextAlignment, or TextStyle. - /// - private static TextLine PerformTextTrimming(IReadOnlyList textRuns, TextRange textRange, - double paragraphWidth, TextParagraphProperties paragraphProperties) - { - var textTrimming = paragraphProperties.TextTrimming; - var availableWidth = paragraphWidth; - var currentWidth = 0.0; - var runIndex = 0; - - while (runIndex < textRuns.Count) - { - var currentRun = textRuns[runIndex]; - - currentWidth += currentRun.GlyphRun.Bounds.Width; - - if (currentWidth > availableWidth) - { - var ellipsisRun = CreateEllipsisRun(currentRun.Properties); - - var measuredLength = MeasureText(currentRun, availableWidth - ellipsisRun.GlyphRun.Bounds.Width); - - if (textTrimming == TextTrimming.WordEllipsis) - { - if (measuredLength < textRange.End) - { - var currentBreakPosition = 0; - - var lineBreaker = new LineBreakEnumerator(currentRun.Text); - - while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) - { - var nextBreakPosition = lineBreaker.Current.PositionWrap; - - if (nextBreakPosition == 0) - { - break; - } - - if (nextBreakPosition > measuredLength) - { - break; - } - - currentBreakPosition = nextBreakPosition; - } - - measuredLength = currentBreakPosition; - } - } - - var splitResult = SplitTextRuns(textRuns, measuredLength); - - var trimmedRuns = new List(splitResult.First.Count + 1); - - trimmedRuns.AddRange(splitResult.First); - - trimmedRuns.Add(ellipsisRun); - - var textLineMetrics = - TextLineMetrics.Create(trimmedRuns, textRange, paragraphWidth, paragraphProperties); - - return new TextLineImpl(trimmedRuns, textLineMetrics); - } - - availableWidth -= currentRun.GlyphRun.Bounds.Width; - - runIndex++; - } - - return new TextLineImpl(textRuns, - TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties)); - } - /// /// Performs text wrapping returns a list of text lines. /// @@ -269,7 +330,7 @@ namespace Avalonia.Media.TextFormatting var availableWidth = paragraphWidth; var currentWidth = 0.0; var runIndex = 0; - var length = 0; + var currentLength = 0; while (runIndex < textRuns.Count) { @@ -277,60 +338,55 @@ namespace Avalonia.Media.TextFormatting if (currentWidth + currentRun.GlyphRun.Bounds.Width > availableWidth) { - var measuredLength = MeasureText(currentRun, paragraphWidth - currentWidth); + var measuredLength = MeasureCharacters(currentRun, paragraphWidth - currentWidth); + + var breakFound = false; + + var currentBreakPosition = 0; if (measuredLength < currentRun.Text.Length) { - if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow) - { - var lineBreaker = new LineBreakEnumerator(currentRun.Text.Skip(measuredLength)); + var lineBreaker = new LineBreakEnumerator(currentRun.Text); - if (lineBreaker.MoveNext()) - { - measuredLength += lineBreaker.Current.PositionWrap; - } - else - { - measuredLength = currentRun.Text.Length; - } - } - else + while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) { - var currentBreakPosition = -1; + var nextBreakPosition = lineBreaker.Current.PositionWrap; - var lineBreaker = new LineBreakEnumerator(currentRun.Text); - - while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) + if (nextBreakPosition == 0 || nextBreakPosition > measuredLength) { - var nextBreakPosition = lineBreaker.Current.PositionWrap; + break; + } - if (nextBreakPosition == 0) - { - break; - } + breakFound = lineBreaker.Current.Required || + lineBreaker.Current.PositionWrap != currentRun.Text.Length; - if (nextBreakPosition > measuredLength) - { - break; - } + currentBreakPosition = nextBreakPosition; + } + } - currentBreakPosition = nextBreakPosition; - } + if (breakFound) + { + measuredLength = currentBreakPosition; + } + else + { + if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow) + { + var lineBreaker = new LineBreakEnumerator(currentRun.Text.Skip(currentBreakPosition)); - if (currentBreakPosition != -1) + if (lineBreaker.MoveNext()) { - measuredLength = currentBreakPosition; + measuredLength = currentBreakPosition + lineBreaker.Current.PositionWrap; } - } } - length += measuredLength; + currentLength += measuredLength; - var splitResult = SplitTextRuns(textRuns, length); + var splitResult = SplitTextRuns(textRuns, currentLength); var textLineMetrics = TextLineMetrics.Create(splitResult.First, - new TextRange(textRange.Start, length), paragraphWidth, paragraphProperties); + new TextRange(textRange.Start, currentLength), paragraphWidth, paragraphProperties); var lineBreak = splitResult.Second != null && splitResult.Second.Count > 0 ? new TextLineBreak(splitResult.Second) : @@ -341,7 +397,7 @@ namespace Avalonia.Media.TextFormatting currentWidth += currentRun.GlyphRun.Bounds.Width; - length += currentRun.GlyphRun.Characters.Length; + currentLength += currentRun.GlyphRun.Characters.Length; runIndex++; } @@ -350,94 +406,6 @@ namespace Avalonia.Media.TextFormatting TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties)); } - /// - /// Measures the number of characters that fits into available width. - /// - /// The text run. - /// The available width. - /// - private static int MeasureText(ShapedTextCharacters textCharacters, double availableWidth) - { - var glyphRun = textCharacters.GlyphRun; - - if (glyphRun.Bounds.Width < availableWidth) - { - return glyphRun.Characters.Length; - } - - var glyphCount = 0; - - var currentWidth = 0.0; - - if (glyphRun.GlyphAdvances.IsEmpty) - { - var glyphTypeface = glyphRun.GlyphTypeface; - - for (var i = 0; i < glyphRun.GlyphClusters.Length; i++) - { - var glyph = glyphRun.GlyphIndices[i]; - - var advance = glyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale; - - if (currentWidth + advance > availableWidth) - { - break; - } - - currentWidth += advance; - - glyphCount++; - } - } - else - { - for (var i = 0; i < glyphRun.GlyphAdvances.Length; i++) - { - var advance = glyphRun.GlyphAdvances[i]; - - if (currentWidth + advance > availableWidth) - { - break; - } - - currentWidth += advance; - - glyphCount++; - } - } - - if (glyphCount == glyphRun.GlyphIndices.Length) - { - return glyphRun.Characters.Length; - } - - if (glyphRun.GlyphClusters.IsEmpty) - { - return glyphCount; - } - - var firstCluster = glyphRun.GlyphClusters[0]; - - var lastCluster = glyphRun.GlyphClusters[glyphCount]; - - return lastCluster - firstCluster; - } - - /// - /// Creates an ellipsis. - /// - /// The text run properties. - /// - private static ShapedTextCharacters CreateEllipsisRun(TextRunProperties properties) - { - var formatterImpl = AvaloniaLocator.Current.GetService(); - - var glyphRun = formatterImpl.ShapeText(s_ellipsis, properties.Typeface, properties.FontRenderingEmSize, - properties.CultureInfo); - - return new ShapedTextCharacters(glyphRun, properties); - } - /// /// Gets the text range that is covered by the text runs. /// @@ -464,86 +432,7 @@ namespace Avalonia.Media.TextFormatting return new TextRange(start, end - start); } - /// - /// Split a sequence of runs into two segments at specified length. - /// - /// The text run's. - /// The length to split at. - /// The split text runs. - private static SplitTextRunsResult SplitTextRuns(IReadOnlyList textRuns, int length) - { - var currentLength = 0; - - for (var i = 0; i < textRuns.Count; i++) - { - var currentRun = textRuns[i]; - - if (currentLength + currentRun.GlyphRun.Characters.Length < length) - { - currentLength += currentRun.GlyphRun.Characters.Length; - continue; - } - - var firstCount = currentRun.GlyphRun.Characters.Length >= 1 ? i + 1 : i; - - var first = new ShapedTextCharacters[firstCount]; - - if (firstCount > 1) - { - for (var j = 0; j < i; j++) - { - first[j] = textRuns[j]; - } - } - - var secondCount = textRuns.Count - firstCount; - - if (currentLength + currentRun.GlyphRun.Characters.Length == length) - { - var second = new ShapedTextCharacters[secondCount]; - - var offset = currentRun.GlyphRun.Characters.Length > 1 ? 1 : 0; - - if (secondCount > 0) - { - for (var j = 0; j < secondCount; j++) - { - second[j] = textRuns[i + j + offset]; - } - } - - first[i] = currentRun; - - return new SplitTextRunsResult(first, second); - } - else - { - secondCount++; - - var second = new ShapedTextCharacters[secondCount]; - - if (secondCount > 0) - { - for (var j = 1; j < secondCount; j++) - { - second[j] = textRuns[i + j]; - } - } - - var split = currentRun.Split(length - currentLength); - - first[i] = split.First; - - second[0] = split.Second; - - return new SplitTextRunsResult(first, second); - } - } - - return new SplitTextRunsResult(textRuns, null); - } - - private readonly struct SplitTextRunsResult + internal readonly struct SplitTextRunsResult { public SplitTextRunsResult(IReadOnlyList first, IReadOnlyList second) { diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs index 54745144c8..14602a2560 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Utilities; -using Avalonia.Platform; namespace Avalonia.Media.TextFormatting { @@ -17,6 +15,7 @@ namespace Avalonia.Media.TextFormatting private readonly ReadOnlySlice _text; private readonly TextParagraphProperties _paragraphProperties; private readonly IReadOnlyList> _textStyleOverrides; + private readonly TextTrimming _textTrimming; /// /// Initializes a new instance of the class. @@ -54,9 +53,11 @@ namespace Avalonia.Media.TextFormatting new ReadOnlySlice(text.AsMemory()); _paragraphProperties = - CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping, textTrimming, + CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping, textDecorations, lineHeight); + _textTrimming = textTrimming; + _textStyleOverrides = textStyleOverrides; LineHeight = lineHeight; @@ -143,18 +144,16 @@ namespace Avalonia.Media.TextFormatting /// The foreground. /// The text alignment. /// The text wrapping. - /// The text trimming. /// The text decorations. /// The height of each line of text. /// private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize, - IBrush foreground, TextAlignment textAlignment, TextWrapping textWrapping, TextTrimming textTrimming, + IBrush foreground, TextAlignment textAlignment, TextWrapping textWrapping, TextDecorationCollection textDecorations, double lineHeight) { var textRunStyle = new GenericTextRunProperties(typeface, fontSize, textDecorations, foreground); - return new GenericTextParagraphProperties(textRunStyle, textAlignment, textWrapping, textTrimming, - lineHeight); + return new GenericTextParagraphProperties(textRunStyle, textAlignment, textWrapping, lineHeight); } /// @@ -214,25 +213,44 @@ namespace Avalonia.Media.TextFormatting var textSource = new FormattedTextSource(_text, _paragraphProperties.DefaultTextRunProperties, _textStyleOverrides); - TextLineBreak previousLineBreak = null; + TextLine previousLine = null; - while (currentPosition < _text.Length && (MaxLines == 0 || textLines.Count < MaxLines)) + while (currentPosition < _text.Length) { var textLine = TextFormatter.Current.FormatLine(textSource, currentPosition, MaxWidth, - _paragraphProperties, previousLineBreak); + _paragraphProperties, previousLine?.LineBreak); - previousLineBreak = textLine.LineBreak; + currentPosition += textLine.TextRange.Length; - textLines.Add(textLine); + if (textLines.Count > 0) + { + if (textLines.Count == MaxLines || !double.IsPositiveInfinity(MaxHeight) && + height + textLine.LineMetrics.Size.Height > MaxHeight) + { + if (previousLine?.LineBreak != null && _textTrimming != TextTrimming.None) + { + var collapsedLine = + previousLine.Collapse(GetCollapsingProperties(MaxWidth)); - UpdateBounds(textLine, ref width, ref height); + textLines[textLines.Count - 1] = collapsedLine; + } + + break; + } + } + + var hasOverflowed = textLine.LineMetrics.HasOverflowed; - if (!double.IsPositiveInfinity(MaxHeight) && height > MaxHeight) + if (hasOverflowed && _textTrimming != TextTrimming.None) { - break; + textLine = textLine.Collapse(GetCollapsingProperties(MaxWidth)); } - currentPosition += textLine.TextRange.Length; + textLines.Add(textLine); + + UpdateBounds(textLine, ref width, ref height); + + previousLine = textLine; if (currentPosition != _text.Length || textLine.LineBreak == null) { @@ -250,6 +268,23 @@ namespace Avalonia.Media.TextFormatting } } + /// + /// Gets the for current text trimming mode. + /// + /// The collapsing width. + /// The . + private TextCollapsingProperties GetCollapsingProperties(double width) + { + return _textTrimming switch + { + TextTrimming.CharacterEllipsis => new TextTrailingCharacterEllipsis(width, + _paragraphProperties.DefaultTextRunProperties), + TextTrimming.WordEllipsis => new TextTrailingWordEllipsis(width, + _paragraphProperties.DefaultTextRunProperties), + _ => throw new ArgumentOutOfRangeException(), + }; + } + private readonly struct FormattedTextSource : ITextSource { private readonly ReadOnlySlice _text; diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs index c3b7dfc77a..423ca9fb7f 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs @@ -39,6 +39,14 @@ namespace Avalonia.Media.TextFormatting /// public abstract TextLineBreak LineBreak { get; } + /// + /// Gets a value that indicates whether the line is collapsed. + /// + /// + /// true, if the line is collapsed; otherwise, false. + /// + public abstract bool HasCollapsed { get; } + /// /// Draws the at the given origin. /// @@ -47,40 +55,49 @@ namespace Avalonia.Media.TextFormatting public abstract void Draw(DrawingContext drawingContext, Point origin); /// - /// Client to get the character hit corresponding to the specified - /// distance from the beginning of the line. + /// Create a collapsed line based on collapsed text properties. + /// + /// A list of + /// objects that represent the collapsed text properties. + /// + /// A value that represents a collapsed line that can be displayed. + /// + public abstract TextLine Collapse(params TextCollapsingProperties[] collapsingPropertiesList); + + /// + /// Gets the character hit corresponding to the specified distance from the beginning of the line. /// - /// distance in text flow direction from the beginning of the line - /// The + /// A value that represents the distance from the beginning of the line. + /// The object at the specified distance from the beginning of the line. public abstract CharacterHit GetCharacterHitFromDistance(double distance); /// - /// Client to get the distance from the beginning of the line from the specified + /// Gets the distance from the beginning of the line to the specified character hit. /// . /// - /// of the character to query the distance. - /// Distance in text flow direction from the beginning of the line. + /// The object whose distance you want to query. + /// A that represents the distance from the beginning of the line. public abstract double GetDistanceFromCharacterHit(CharacterHit characterHit); /// - /// Client to get the next for caret navigation. + /// Gets the next character hit for caret navigation. /// /// The current . /// The next . public abstract CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit); /// - /// Client to get the previous character hit for caret navigation + /// Gets the previous character hit for caret navigation. /// - /// the current character hit - /// The previous + /// The current . + /// The previous . public abstract CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit); /// - /// Client to get the previous character hit after backspacing + /// Gets the previous character hit after backspacing. /// - /// the current character hit - /// The after backspacing + /// The current . + /// The after backspacing. public abstract CharacterHit GetBackspaceCaretCharacterHit(CharacterHit characterHit); /// diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs index a1a9b50793..f73a7be759 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using Avalonia.Media.TextFormatting.Unicode; +using Avalonia.Platform; namespace Avalonia.Media.TextFormatting { @@ -7,11 +9,12 @@ namespace Avalonia.Media.TextFormatting private readonly IReadOnlyList _textRuns; public TextLineImpl(IReadOnlyList textRuns, TextLineMetrics lineMetrics, - TextLineBreak lineBreak = null) + TextLineBreak lineBreak = null, bool hasCollapsed = false) { _textRuns = textRuns; LineMetrics = lineMetrics; LineBreak = lineBreak; + HasCollapsed = hasCollapsed; } /// @@ -26,6 +29,9 @@ namespace Avalonia.Media.TextFormatting /// public override TextLineBreak LineBreak { get; } + /// + public override bool HasCollapsed { get; } + /// public override void Draw(DrawingContext drawingContext, Point origin) { @@ -41,6 +47,99 @@ namespace Avalonia.Media.TextFormatting } } + /// + public override TextLine Collapse(params TextCollapsingProperties[] collapsingPropertiesList) + { + if (collapsingPropertiesList == null || collapsingPropertiesList.Length == 0) + { + return this; + } + + var collapsingProperties = collapsingPropertiesList[0]; + var runIndex = 0; + var currentWidth = 0.0; + var textRange = TextRange; + var collapsedLength = 0; + TextLineMetrics textLineMetrics; + + var shapedSymbol = CreateShapedSymbol(collapsingProperties.Symbol); + + var availableWidth = collapsingProperties.Width - shapedSymbol.Bounds.Width; + + while (runIndex < _textRuns.Count) + { + var currentRun = _textRuns[runIndex]; + + currentWidth += currentRun.GlyphRun.Bounds.Width; + + if (currentWidth > availableWidth) + { + var measuredLength = TextFormatterImpl.MeasureCharacters(currentRun, availableWidth); + + var currentBreakPosition = 0; + + if (measuredLength < textRange.End) + { + var lineBreaker = new LineBreakEnumerator(currentRun.Text); + + while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) + { + var nextBreakPosition = lineBreaker.Current.PositionWrap; + + if (nextBreakPosition == 0) + { + break; + } + + if (nextBreakPosition > measuredLength) + { + break; + } + + currentBreakPosition = nextBreakPosition; + } + } + + if (collapsingProperties.Style == TextCollapsingStyle.TrailingWord) + { + measuredLength = currentBreakPosition; + } + + collapsedLength += measuredLength; + + var splitResult = TextFormatterImpl.SplitTextRuns(_textRuns, collapsedLength); + + var shapedTextCharacters = new List(splitResult.First.Count + 1); + + shapedTextCharacters.AddRange(splitResult.First); + + shapedTextCharacters.Add(shapedSymbol); + + textRange = new TextRange(textRange.Start, collapsedLength); + + var shapedWidth = GetShapedWidth(shapedTextCharacters); + + textLineMetrics = new TextLineMetrics(new Size(shapedWidth, LineMetrics.Size.Height), + LineMetrics.TextBaseline, textRange, false); + + return new TextLineImpl(shapedTextCharacters, textLineMetrics, LineBreak, true); + } + + availableWidth -= currentRun.GlyphRun.Bounds.Width; + + collapsedLength += currentRun.GlyphRun.Characters.Length; + + runIndex++; + } + + textLineMetrics = + new TextLineMetrics(LineMetrics.Size.WithWidth(LineMetrics.Size.Width + shapedSymbol.Bounds.Width), + LineMetrics.TextBaseline, TextRange, LineMetrics.HasOverflowed); + + return new TextLineImpl(new List(_textRuns) { shapedSymbol }, textLineMetrics, null, + true); + } + /// public override CharacterHit GetCharacterHitFromDistance(double distance) { @@ -82,7 +181,7 @@ namespace Avalonia.Media.TextFormatting return nextCharacterHit; } - return new CharacterHit(TextRange.End); // Can't move, we're after the last character + return characterHit; // Can't move, we're after the last character } /// @@ -93,7 +192,7 @@ namespace Avalonia.Media.TextFormatting return previousCharacterHit; } - return new CharacterHit(TextRange.Start); // Can't move, we're before the first character + return characterHit; // Can't move, we're before the first character } /// @@ -148,9 +247,13 @@ namespace Avalonia.Media.TextFormatting { var run = _textRuns[runIndex]; - nextCharacterHit = run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _); + var foundCharacterHit = run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _); - if (codepointIndex <= nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength) + nextCharacterHit = characterHit.TrailingLength != 0 ? + foundCharacterHit : + new CharacterHit(foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength); + + if (nextCharacterHit.FirstCharacterIndex > characterHit.FirstCharacterIndex) { return true; } @@ -184,9 +287,13 @@ namespace Avalonia.Media.TextFormatting { var run = _textRuns[runIndex]; - previousCharacterHit = run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _); + var foundCharacterHit = run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _); + + previousCharacterHit = characterHit.TrailingLength != 0 ? + foundCharacterHit : + new CharacterHit(foundCharacterHit.FirstCharacterIndex); - if (previousCharacterHit.FirstCharacterIndex < codepointIndex) + if (previousCharacterHit.FirstCharacterIndex < characterHit.FirstCharacterIndex) { return true; } @@ -230,5 +337,41 @@ namespace Avalonia.Media.TextFormatting return runIndex; } + + /// + /// Creates a shaped symbol. + /// + /// The symbol run to shape. + /// + /// The shaped symbol. + /// + internal static ShapedTextCharacters CreateShapedSymbol(TextRun textRun) + { + var formatterImpl = AvaloniaLocator.Current.GetService(); + + var glyphRun = formatterImpl.ShapeText(textRun.Text, textRun.Properties.Typeface, textRun.Properties.FontRenderingEmSize, + textRun.Properties.CultureInfo); + + return new ShapedTextCharacters(glyphRun, textRun.Properties); + } + + /// + /// Gets the shaped width of specified shaped text characters. + /// + /// The shaped text characters. + /// + /// The shaped width. + /// + private static double GetShapedWidth(IReadOnlyList shapedTextCharacters) + { + var shapedWidth = 0.0; + + for (var i = 0; i < shapedTextCharacters.Count; i++) + { + shapedWidth += shapedTextCharacters[i].Bounds.Width; + } + + return shapedWidth; + } } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs index 2f7809ff35..6875cc1c04 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs @@ -9,11 +9,12 @@ namespace Avalonia.Media.TextFormatting /// public readonly struct TextLineMetrics { - public TextLineMetrics(Size size, double textBaseline, TextRange textRange) + public TextLineMetrics(Size size, double textBaseline, TextRange textRange, bool hasOverflowed) { Size = size; TextBaseline = textBaseline; TextRange = textRange; + HasOverflowed = hasOverflowed; } /// @@ -37,6 +38,12 @@ namespace Avalonia.Media.TextFormatting /// public double TextBaseline { get; } + /// + /// Gets a boolean value that indicates whether content of the line overflows + /// the specified paragraph width. + /// + public bool HasOverflowed { get; } + /// /// Creates the text line metrics. /// @@ -83,7 +90,7 @@ namespace Avalonia.Media.TextFormatting descent - ascent + lineGap : paragraphProperties.LineHeight); - return new TextLineMetrics(size, -ascent, textRange); + return new TextLineMetrics(size, -ascent, textRange, size.Width > paragraphWidth); } } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs index 39eb695404..3ecd1aafd9 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs @@ -26,11 +26,6 @@ /// public abstract TextWrapping TextWrapping { get; } - /// - /// Gets the text trimming. - /// - public abstract TextTrimming TextTrimming { get; } - /// /// Paragraph's line height /// diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs index bbcdfe2d8e..c4f9443c3d 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs @@ -4,12 +4,11 @@ using System.Globalization; namespace Avalonia.Media.TextFormatting { /// - /// Properties that can change from one run to the next, such as typeface or foreground brush. + /// Provides a set of properties, such as typeface or foreground brush, that can be applied to a TextRun object. This is an abstract class. /// /// - /// The client provides a concrete implementation of this abstract run properties class. This - /// allows client to implement their run properties the way that fits with their run formatting - /// store. + /// The text layout client provides a concrete implementation of this abstract class. + /// This enables the client to implement text run properties in a way that corresponds with the associated formatting store. /// public abstract class TextRunProperties : IEquatable { diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs new file mode 100644 index 0000000000..4bd46e8c75 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs @@ -0,0 +1,33 @@ +using Avalonia.Utilities; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// a collapsing properties to collapse whole line toward the end + /// at character granularity and with ellipsis being the collapsing symbol + /// + public class TextTrailingCharacterEllipsis : TextCollapsingProperties + { + private static readonly ReadOnlySlice s_ellipsis = new ReadOnlySlice(new[] { '\u2026' }); + + /// + /// Construct a text trailing character ellipsis collapsing properties + /// + /// width in which collapsing is constrained to + /// text run properties of ellispis symbol + public TextTrailingCharacterEllipsis(double width, TextRunProperties textRunProperties) + { + Width = width; + Symbol = new TextCharacters(s_ellipsis, textRunProperties); + } + + /// + public sealed override double Width { get; } + + /// + public sealed override TextRun Symbol { get; } + + /// + public sealed override TextCollapsingStyle Style { get; } = TextCollapsingStyle.TrailingCharacter; + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs new file mode 100644 index 0000000000..9dffddd207 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs @@ -0,0 +1,37 @@ +using Avalonia.Utilities; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// a collapsing properties to collapse whole line toward the end + /// at word granularity and with ellipsis being the collapsing symbol + /// + public class TextTrailingWordEllipsis : TextCollapsingProperties + { + private static readonly ReadOnlySlice s_ellipsis = new ReadOnlySlice(new[] { '\u2026' }); + + /// + /// Construct a text trailing word ellipsis collapsing properties + /// + /// width in which collapsing is constrained to + /// text run properties of ellispis symbol + public TextTrailingWordEllipsis( + double width, + TextRunProperties textRunProperties + ) + { + Width = width; + Symbol = new TextCharacters(s_ellipsis, textRunProperties); + } + + + /// + public sealed override double Width { get; } + + /// + public sealed override TextRun Symbol { get; } + + /// + public sealed override TextCollapsingStyle Style { get; } = TextCollapsingStyle.TrailingWord; + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs index 20fe345d93..2f46fdd9d0 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs @@ -112,7 +112,7 @@ namespace Avalonia.Media.TextFormatting.Unicode { count = 1; - if (index > text.End) + if (index > text.Length) { return ReplacementCodepoint; } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs index 26f7721128..76bb9ac44f 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs @@ -109,7 +109,6 @@ namespace Avalonia.Media.TextFormatting.Unicode { case PairBreakType.DI: // Direct break shouldBreak = true; - _lastPos = _pos; break; case PairBreakType.IN: // possible indirect break diff --git a/src/Avalonia.Visuals/Media/TextWrapping.cs b/src/Avalonia.Visuals/Media/TextWrapping.cs index d649bda23f..b7915e5612 100644 --- a/src/Avalonia.Visuals/Media/TextWrapping.cs +++ b/src/Avalonia.Visuals/Media/TextWrapping.cs @@ -5,13 +5,6 @@ namespace Avalonia.Media /// public enum TextWrapping { - /// - /// Line-breaking occurs if the line overflows the available block width. - /// However, a line may overflow the block width if the line breaking algorithm - /// cannot determine a break opportunity, as in the case of a very long word. - /// - WrapWithOverflow, - /// /// Text should not wrap. /// @@ -20,6 +13,13 @@ namespace Avalonia.Media /// /// Text can wrap. /// - Wrap + Wrap, + + /// + /// Line-breaking occurs if the line overflows the available block width. + /// However, a line may overflow the block width if the line breaking algorithm + /// cannot determine a break opportunity, as in the case of a very long word. + /// + WrapWithOverflow } } diff --git a/src/Avalonia.X11/NativeDialogs/Gtk.cs b/src/Avalonia.X11/NativeDialogs/Gtk.cs index 82cf3c934f..77e410162f 100644 --- a/src/Avalonia.X11/NativeDialogs/Gtk.cs +++ b/src/Avalonia.X11/NativeDialogs/Gtk.cs @@ -204,6 +204,9 @@ namespace Avalonia.X11.NativeDialogs [DllImport(GtkName)] public static extern IntPtr gtk_file_chooser_add_filter(IntPtr chooser, IntPtr filter); + + [DllImport(GtkName)] + public static extern IntPtr gtk_file_chooser_get_filter(IntPtr chooser); [DllImport(GtkName)] public static extern void gtk_widget_realize(IntPtr gtkWidget); diff --git a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs index 287a541bc8..6cf6c6f35f 100644 --- a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs +++ b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs @@ -16,7 +16,7 @@ namespace Avalonia.X11.NativeDialogs { private Task _initialized; private unsafe Task ShowDialog(string title, IWindowImpl parent, GtkFileChooserAction action, - bool multiSelect, string initialFileName, IEnumerable filters) + bool multiSelect, string initialFileName, IEnumerable filters, string defaultExtension) { IntPtr dlg; using (var name = new Utf8Buffer(title)) @@ -35,11 +35,13 @@ namespace Avalonia.X11.NativeDialogs foreach (var d in disposables) d.Dispose(); disposables.Clear(); } - + + var filtersDic = new Dictionary(); if(filters != null) foreach (var f in filters) { var filter = gtk_file_filter_new(); + filtersDic[filter] = f; using (var b = new Utf8Buffer(f.Name)) gtk_file_filter_set_name(filter, b); @@ -74,6 +76,15 @@ namespace Avalonia.X11.NativeDialogs } g_slist_free(gs); result = resultList.ToArray(); + + // GTK doesn't auto-append the extension, so we need to do that manually + if (action == GtkFileChooserAction.Save) + { + var currentFilter = gtk_file_chooser_get_filter(dlg); + filtersDic.TryGetValue(currentFilter, out var selectedFilter); + for (var c = 0; c < result.Length; c++) + result[c] = NameWithExtension(result[c], defaultExtension, selectedFilter); + } } gtk_widget_hide(dlg); @@ -101,7 +112,30 @@ namespace Avalonia.X11.NativeDialogs gtk_window_present(dlg); return tcs.Task; } - + + string NameWithExtension(string path, string defaultExtension, FileDialogFilter filter) + { + var name = Path.GetFileName(path); + if (name != null && !name.Contains(".")) + { + if (filter?.Extensions?.Count > 0) + { + if (defaultExtension != null + && filter.Extensions.Contains(defaultExtension)) + return path + "." + defaultExtension.TrimStart('.'); + + var ext = filter.Extensions.FirstOrDefault(x => x != "*"); + if (ext != null) + return path + "." + ext.TrimStart('.'); + } + + if (defaultExtension != null) + path += "." + defaultExtension.TrimStart('.'); + } + + return path; + } + public async Task ShowFileDialogAsync(FileDialog dialog, Window parent) { await EnsureInitialized(); @@ -113,7 +147,8 @@ namespace Avalonia.X11.NativeDialogs dialog is OpenFileDialog ? GtkFileChooserAction.Open : GtkFileChooserAction.Save, (dialog as OpenFileDialog)?.AllowMultiple ?? false, Path.Combine(string.IsNullOrEmpty(dialog.Directory) ? "" : dialog.Directory, - string.IsNullOrEmpty(dialog.InitialFileName) ? "" : dialog.InitialFileName), dialog.Filters)); + string.IsNullOrEmpty(dialog.InitialFileName) ? "" : dialog.InitialFileName), dialog.Filters, + (dialog as SaveFileDialog)?.DefaultExtension)); } public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) @@ -125,7 +160,7 @@ namespace Avalonia.X11.NativeDialogs return await await RunOnGlibThread(async () => { var res = await ShowDialog(dialog.Title, platformImpl, - GtkFileChooserAction.SelectFolder, false, dialog.Directory, null); + GtkFileChooserAction.SelectFolder, false, dialog.Directory, null, null); return res?.FirstOrDefault(); }); } diff --git a/src/Avalonia.X11/X11NativeControlHost.cs b/src/Avalonia.X11/X11NativeControlHost.cs index 23fb27f72b..6c4eb81c84 100644 --- a/src/Avalonia.X11/X11NativeControlHost.cs +++ b/src/Avalonia.X11/X11NativeControlHost.cs @@ -167,7 +167,7 @@ namespace Avalonia.X11 XUnmapWindow(_display, _holder.Handle); } - size *= _attachedTo.Window.Scaling; + size *= _attachedTo.Window.RenderScaling; XResizeWindow(_display, _child.Handle, Math.Max(1, (int)size.Width), Math.Max(1, (int)size.Height)); } @@ -179,7 +179,7 @@ namespace Avalonia.X11 CheckDisposed(); if (_attachedTo == null) throw new InvalidOperationException("The control isn't currently attached to a toplevel"); - bounds *= _attachedTo.Window.Scaling; + bounds *= _attachedTo.Window.RenderScaling; var pixelRect = new PixelRect((int)bounds.X, (int)bounds.Y, Math.Max(1, (int)bounds.Width), Math.Max(1, (int)bounds.Height)); diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 2a13999e8d..c24abcd230 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -163,7 +163,7 @@ namespace Avalonia.X11 var surfaces = new List { new X11FramebufferSurface(_x11.DeferredDisplay, _renderHandle, - depth, () => Scaling) + depth, () => RenderScaling) }; if (egl != null) @@ -217,7 +217,7 @@ namespace Avalonia.X11 } } - public double Scaling => _window.Scaling; + public double Scaling => _window.RenderScaling; } void UpdateMotifHints() @@ -284,9 +284,9 @@ namespace Avalonia.X11 XSetWMNormalHints(_x11.Display, _handle, ref hints); } - public Size ClientSize => new Size(_realSize.Width / Scaling, _realSize.Height / Scaling); + public Size ClientSize => new Size(_realSize.Width / RenderScaling, _realSize.Height / RenderScaling); - public double Scaling + public double RenderScaling { get { @@ -296,6 +296,8 @@ namespace Avalonia.X11 } private set => _scaling = value; } + + public double DesktopScaling => RenderScaling; public IEnumerable Surfaces { get; } public Action Input { get; set; } @@ -538,14 +540,14 @@ namespace Avalonia.X11 { var monitor = _platform.X11Screens.Screens.OrderBy(x => x.PixelDensity) .FirstOrDefault(m => m.Bounds.Contains(Position)); - newScaling = monitor?.PixelDensity ?? Scaling; + newScaling = monitor?.PixelDensity ?? RenderScaling; } - if (Scaling != newScaling) + if (RenderScaling != newScaling) { var oldScaledSize = ClientSize; - Scaling = newScaling; - ScalingChanged?.Invoke(Scaling); + RenderScaling = newScaling; + ScalingChanged?.Invoke(RenderScaling); SetMinMaxSize(_scaledMinMaxSize.minSize, _scaledMinMaxSize.maxSize); if(!skipResize) Resize(oldScaledSize, true); @@ -707,9 +709,9 @@ namespace Avalonia.X11 private void ScheduleInput(RawInputEventArgs args) { if (args is RawPointerEventArgs mouse) - mouse.Position = mouse.Position / Scaling; + mouse.Position = mouse.Position / RenderScaling; if (args is RawDragEvent drag) - drag.Location = drag.Location / Scaling; + drag.Location = drag.Location / RenderScaling; _lastEvent = new InputEventContainer() {Event = args}; _inputQueue.Enqueue(_lastEvent); @@ -760,11 +762,7 @@ namespace Avalonia.X11 public void Dispose() { - if (_handle != IntPtr.Zero) - { - XDestroyWindow(_x11.Display, _handle); - Cleanup(); - } + Cleanup(); } void Cleanup() @@ -787,8 +785,7 @@ namespace Avalonia.X11 } if (_useRenderWindow && _renderHandle != IntPtr.Zero) - { - XDestroyWindow(_x11.Display, _renderHandle); + { _renderHandle = IntPtr.Zero; } } @@ -821,11 +818,11 @@ namespace Avalonia.X11 public void Hide() => XUnmapWindow(_x11.Display, _handle); - public Point PointToClient(PixelPoint point) => new Point((point.X - Position.X) / Scaling, (point.Y - Position.Y) / Scaling); + public Point PointToClient(PixelPoint point) => new Point((point.X - Position.X) / RenderScaling, (point.Y - Position.Y) / RenderScaling); public PixelPoint PointToScreen(Point point) => new PixelPoint( - (int)(point.X * Scaling + Position.X), - (int)(point.Y * Scaling + Position.Y)); + (int)(point.X * RenderScaling + Position.X), + (int)(point.Y * RenderScaling + Position.Y)); public void SetSystemDecorations(SystemDecorations enabled) { @@ -845,7 +842,7 @@ namespace Avalonia.X11 Resize(size, true); } - PixelSize ToPixelSize(Size size) => new PixelSize((int)(size.Width * Scaling), (int)(size.Height * Scaling)); + PixelSize ToPixelSize(Size size) => new PixelSize((int)(size.Width * RenderScaling), (int)(size.Height * RenderScaling)); void Resize(Size clientSize, bool force) { @@ -1025,13 +1022,13 @@ namespace Avalonia.X11 { _scaledMinMaxSize = (minSize, maxSize); var min = new PixelSize( - (int)(minSize.Width < 1 ? 1 : minSize.Width * Scaling), - (int)(minSize.Height < 1 ? 1 : minSize.Height * Scaling)); + (int)(minSize.Width < 1 ? 1 : minSize.Width * RenderScaling), + (int)(minSize.Height < 1 ? 1 : minSize.Height * RenderScaling)); const int maxDim = MaxWindowDimension; var max = new PixelSize( - (int)(maxSize.Width > maxDim ? maxDim : Math.Max(min.Width, maxSize.Width * Scaling)), - (int)(maxSize.Height > maxDim ? maxDim : Math.Max(min.Height, maxSize.Height * Scaling))); + (int)(maxSize.Width > maxDim ? maxDim : Math.Max(min.Width, maxSize.Width * RenderScaling)), + (int)(maxSize.Height > maxDim ? maxDim : Math.Max(min.Height, maxSize.Height * RenderScaling))); _minMaxSize = (min, max); UpdateSizeHints(null); diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index b8ae2eb4d8..0a101eec7a 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -65,7 +65,7 @@ namespace Avalonia.LinuxFramebuffer public IMouseDevice MouseDevice => new MouseDevice(); public IPopupImpl CreatePopup() => null; - public double Scaling => _outputBackend.Scaling; + public double RenderScaling => _outputBackend.Scaling; public IEnumerable Surfaces { get; } public Action Input { get; set; } public Action Paint { get; set; } @@ -77,7 +77,7 @@ namespace Avalonia.LinuxFramebuffer public Action Closed { get; set; } public Action LostFocus { get; set; } - public Size ScaledSize => _outputBackend.PixelSize.ToSize(Scaling); + public Size ScaledSize => _outputBackend.PixelSize.ToSize(RenderScaling); public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj b/src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj new file mode 100644 index 0000000000..768545eb7e --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj @@ -0,0 +1,14 @@ + + + + netstandard2.0 + true + + + + + + + + + diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaRuntimeXamlLoader.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaRuntimeXamlLoader.cs new file mode 100644 index 0000000000..4569970d01 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaRuntimeXamlLoader.cs @@ -0,0 +1,42 @@ +using System; +using System.IO; +using System.Reflection; +using System.Text; +using Avalonia.Markup.Xaml.XamlIl; +// ReSharper disable CheckNamespace + +namespace Avalonia.Markup.Xaml +{ + public static class AvaloniaRuntimeXamlLoader + { + /// + /// Loads XAML from a string. + /// + /// The string containing the XAML. + /// Default assembly for clr-namespace: + /// + /// The optional instance into which the XAML should be loaded. + /// + /// The loaded object. + public static object Load(string xaml, Assembly localAssembly = null, object rootInstance = null, Uri uri = null, bool designMode = false) + { + Contract.Requires(xaml != null); + + using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(xaml))) + { + return Load(stream, localAssembly, rootInstance, uri, designMode); + } + } + + public static object Load(Stream stream, Assembly localAssembly, object rootInstance = null, Uri uri = null, + bool designMode = false) + => AvaloniaXamlIlRuntimeCompiler.Load(stream, localAssembly, rootInstance, uri, designMode); + + public static object Parse(string xaml, Assembly localAssembly = null) + => Load(xaml, localAssembly); + + public static T Parse(string xaml, Assembly localAssembly = null) + => (T)Parse(xaml, localAssembly); + + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs similarity index 100% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs similarity index 100% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs similarity index 100% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs similarity index 100% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AddNameScopeRegistration.cs similarity index 100% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AddNameScopeRegistration.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaBindingExtensionTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaBindingExtensionTransformer.cs similarity index 100% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaBindingExtensionTransformer.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaBindingExtensionTransformer.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlAvaloniaPropertyResolver.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlAvaloniaPropertyResolver.cs similarity index 100% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlAvaloniaPropertyResolver.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlAvaloniaPropertyResolver.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs similarity index 100% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs similarity index 100% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlCompiledBindingsMetadataRemover.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlCompiledBindingsMetadataRemover.cs similarity index 100% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlCompiledBindingsMetadataRemover.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlCompiledBindingsMetadataRemover.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlConstructorServiceProviderTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlConstructorServiceProviderTransformer.cs similarity index 100% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlConstructorServiceProviderTransformer.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlConstructorServiceProviderTransformer.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs similarity index 100% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs similarity index 93% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs index 5a0d6bac8d..241976241f 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs @@ -1,11 +1,6 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Text; -using Avalonia.Markup.Parsers; -using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; -using Avalonia.Utilities; using XamlX; using XamlX.Ast; using XamlX.Transform; @@ -129,12 +124,13 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers if (itemsCollectionType != null) { - var elementType = itemsCollectionType - .GetAllInterfaces() - .FirstOrDefault(i => - i.GenericTypeDefinition?.Equals(context.Configuration.WellKnownTypes.IEnumerableT) == true) - .GenericArguments[0]; - return new AvaloniaXamlIlDataContextTypeMetadataNode(on, elementType); + foreach (var i in GetAllInterfacesIncludingSelf(itemsCollectionType)) + { + if (i.GenericTypeDefinition?.Equals(context.Configuration.WellKnownTypes.IEnumerableT) == true) + { + return new AvaloniaXamlIlDataContextTypeMetadataNode(on, i.GenericArguments[0]); + } + } } // We can't infer the collection type and the currently calculated type is definitely wrong. // Notify the user that we were unable to infer the data context type if they use a compiled binding. @@ -165,6 +161,15 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers return new AvaloniaXamlIlUninferrableDataContextMetadataNode(on); } + + private static IEnumerable GetAllInterfacesIncludingSelf(IXamlType type) + { + if (type.IsInterface) + yield return type; + + foreach (var i in type.GetAllInterfaces()) + yield return i; + } } [DebuggerDisplay("DataType = {DataContextType}")] diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDesignPropertiesTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDesignPropertiesTransformer.cs similarity index 100% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDesignPropertiesTransformer.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDesignPropertiesTransformer.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlMetadataRemover.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlMetadataRemover.cs similarity index 100% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlMetadataRemover.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlMetadataRemover.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlPropertyPathTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlPropertyPathTransformer.cs similarity index 100% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlPropertyPathTransformer.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlPropertyPathTransformer.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlResolveByNameMarkupExtensionReplacer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlResolveByNameMarkupExtensionReplacer.cs similarity index 100% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlResolveByNameMarkupExtensionReplacer.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlResolveByNameMarkupExtensionReplacer.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlRootObjectScopeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlRootObjectScopeTransformer.cs similarity index 100% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlRootObjectScopeTransformer.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlRootObjectScopeTransformer.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs similarity index 100% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs similarity index 100% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs similarity index 100% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformSyntheticCompiledBindingMembers.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransformSyntheticCompiledBindingMembers.cs similarity index 100% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformSyntheticCompiledBindingMembers.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransformSyntheticCompiledBindingMembers.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransitionsTypeMetadataTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransitionsTypeMetadataTransformer.cs similarity index 100% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransitionsTypeMetadataTransformer.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransitionsTypeMetadataTransformer.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs similarity index 100% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/IgnoredDirectivesTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/IgnoredDirectivesTransformer.cs similarity index 100% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/IgnoredDirectivesTransformer.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/IgnoredDirectivesTransformer.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/XNameTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/XNameTransformer.cs similarity index 100% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/XNameTransformer.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/XNameTransformer.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs similarity index 100% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlBindingPathHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs similarity index 100% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlBindingPathHelper.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs similarity index 100% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlPropertyInfoAccessorFactoryEmitter.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlPropertyInfoAccessorFactoryEmitter.cs similarity index 100% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlPropertyInfoAccessorFactoryEmitter.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlPropertyInfoAccessorFactoryEmitter.cs diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/IncludeXamlIlSre.props b/src/Markup/Avalonia.Markup.Xaml.Loader/IncludeXamlIlSre.props new file mode 100644 index 0000000000..c902fa956a --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/IncludeXamlIlSre.props @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github similarity index 100% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github rename to src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 3979312ce0..24428253aa 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -45,44 +45,10 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -96,8 +62,6 @@ - - diff --git a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs index 5c21037924..e5c6b72d12 100644 --- a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs +++ b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs @@ -10,10 +10,13 @@ namespace Avalonia.Markup.Xaml /// /// Loads XAML for a avalonia application. /// - public class AvaloniaXamlLoader + public static class AvaloniaXamlLoader { - public bool IsDesignMode { get; set; } - + public interface IRuntimeXamlLoader + { + object Load(Stream stream, Assembly localAsm, object o, Uri baseUri, bool designMode); + } + /// /// Loads the XAML into a Avalonia component. /// @@ -32,7 +35,7 @@ namespace Avalonia.Markup.Xaml /// A base URI to use if is relative. /// /// The loaded object. - public object Load(Uri uri, Uri baseUri = null) + public static object Load(Uri uri, Uri baseUri = null) { Contract.Requires(uri != null); @@ -55,52 +58,22 @@ namespace Avalonia.Markup.Xaml if (compiledResult != null) return compiledResult; } - - - var asset = assetLocator.OpenAndGetAssembly(uri, baseUri); - using (var stream = asset.stream) - { - var absoluteUri = uri.IsAbsoluteUri ? uri : new Uri(baseUri, uri); - return Load(stream, asset.assembly, null, absoluteUri); - } - } - - /// - /// Loads XAML from a string. - /// - /// The string containing the XAML. - /// Default assembly for clr-namespace: - /// - /// The optional instance into which the XAML should be loaded. - /// - /// The loaded object. - public object Load(string xaml, Assembly localAssembly = null, object rootInstance = null) - { - Contract.Requires(xaml != null); - using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(xaml))) + // This is intended for unit-tests only + var runtimeLoader = AvaloniaLocator.Current.GetService(); + if (runtimeLoader != null) { - return Load(stream, localAssembly, rootInstance); + var asset = assetLocator.OpenAndGetAssembly(uri, baseUri); + using (var stream = asset.stream) + { + var absoluteUri = uri.IsAbsoluteUri ? uri : new Uri(baseUri, uri); + return runtimeLoader.Load(stream, asset.assembly, null, absoluteUri, false); + } } - } - /// - /// Loads XAML from a stream. - /// - /// The stream containing the XAML. - /// Default assembly for clr-namespace - /// - /// The optional instance into which the XAML should be loaded. - /// - /// The URI of the XAML - /// The loaded object. - public object Load(Stream stream, Assembly localAssembly, object rootInstance = null, Uri uri = null) - => AvaloniaXamlIlRuntimeCompiler.Load(stream, localAssembly, rootInstance, uri, IsDesignMode); - - public static object Parse(string xaml, Assembly localAssembly = null) - => new AvaloniaXamlLoader().Load(xaml, localAssembly); - - public static T Parse(string xaml, Assembly localAssembly = null) - => (T)Parse(xaml, localAssembly); + throw new XamlLoadException( + $"No precompiled XAML found for {uri} (baseUri: {baseUri}), make sure to specify x:Class and include your XAML file as AvaloniaResource"); + } + } } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs index 0cedf4f364..b6137aa89f 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs @@ -25,8 +25,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions if (_loaded == null) { _isLoading = true; - var loader = new AvaloniaXamlLoader(); - _loaded = (IResourceDictionary)loader.Load(Source, _baseUri); + _loaded = (IResourceDictionary)AvaloniaXamlLoader.Load(Source, _baseUri); _isLoading = false; } diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index ea9042f779..607b552c28 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -51,8 +51,7 @@ namespace Avalonia.Markup.Xaml.Styling if (_loaded == null) { _isLoading = true; - var loader = new AvaloniaXamlLoader(); - var loaded = (IStyle)loader.Load(Source, _baseUri); + var loaded = (IStyle)AvaloniaXamlLoader.Load(Source, _baseUri); _loaded = new[] { loaded }; _isLoading = false; } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs index 5663d08412..07c5451135 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs @@ -5,7 +5,7 @@ using Avalonia.Metadata; namespace Avalonia.Markup.Xaml.Templates { - public class DataTemplate : IDataTemplate + public class DataTemplate : IRecyclingDataTemplate { public Type DataType { get; set; } @@ -14,8 +14,6 @@ namespace Avalonia.Markup.Xaml.Templates [TemplateContent] public object Content { get; set; } - public bool SupportsRecycling { get; set; } = true; - public bool Match(object data) { if (DataType == null) @@ -28,6 +26,11 @@ namespace Avalonia.Markup.Xaml.Templates } } - public IControl Build(object data) => TemplateContent.Load(Content).Control; + public IControl Build(object data) => Build(data, null); + + public IControl Build(object data, IControl existing) + { + return existing ?? TemplateContent.Load(Content).Control; + } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs index b96486235a..b8e1c2df80 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs @@ -18,8 +18,6 @@ namespace Avalonia.Markup.Xaml.Templates [AssignBinding] public Binding ItemsSource { get; set; } - public bool SupportsRecycling { get; set; } = true; - public bool Match(object data) { if (DataType == null) diff --git a/src/Markup/Avalonia.Markup/Data/BindingBase.cs b/src/Markup/Avalonia.Markup/Data/BindingBase.cs index 7c4e7b5efe..3dbc83a7df 100644 --- a/src/Markup/Avalonia.Markup/Data/BindingBase.cs +++ b/src/Markup/Avalonia.Markup/Data/BindingBase.cs @@ -137,9 +137,9 @@ namespace Avalonia.Data { Contract.Requires(target != null); - if (!(target is IStyledElement)) + if (!(target is IDataContextProvider)) { - target = anchor as IStyledElement; + target = anchor as IDataContextProvider; if (target == null) { diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index d1f8d6a779..5e630e54a6 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -570,7 +570,7 @@ namespace Avalonia.Skia float constraint = -1; - if (_wrapping != TextWrapping.NoWrap) + if (_wrapping == TextWrapping.Wrap) { constraint = widthConstraint <= 0 ? MAX_LINE_WIDTH : widthConstraint; if (constraint > MAX_LINE_WIDTH) diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index ffe1175567..b0384a1fdf 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -82,7 +82,7 @@ namespace Avalonia.Skia if (codepoint.IsBreakChar) { - if (i < text.End) + if (i + 1 < text.Length) { var nextCodepoint = Codepoint.ReadAt(text, i + 1, out _); diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs index f5d83611bb..3467a33d16 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -75,7 +75,7 @@ namespace Avalonia.Win32.Interop.Wpf private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam, ref bool handled) { if (msg == (int)UnmanagedMethods.WindowsMessage.WM_DPICHANGED) - _ttl.ScalingChanged?.Invoke(_ttl.Scaling); + _ttl.ScalingChanged?.Invoke(_ttl.RenderScaling); return IntPtr.Zero; } @@ -84,7 +84,7 @@ namespace Avalonia.Win32.Interop.Wpf _currentHwndSource?.RemoveHook(_hook); _currentHwndSource = e.NewSource as HwndSource; _currentHwndSource?.AddHook(_hook); - _ttl.ScalingChanged?.Invoke(_ttl.Scaling); + _ttl.ScalingChanged?.Invoke(_ttl.RenderScaling); } public IRenderer CreateRenderer(IRenderRoot root) @@ -102,7 +102,7 @@ namespace Avalonia.Win32.Interop.Wpf Size ITopLevelImpl.ClientSize => _finalSize; IMouseDevice ITopLevelImpl.MouseDevice => _mouse; - double ITopLevelImpl.Scaling => PresentationSource.FromVisual(this)?.CompositionTarget?.TransformToDevice.M11 ?? 1; + double ITopLevelImpl.RenderScaling => PresentationSource.FromVisual(this)?.CompositionTarget?.TransformToDevice.M11 ?? 1; IEnumerable ITopLevelImpl.Surfaces => _surfaces; diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 1aec4f0016..b7c68c4b95 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -1059,9 +1060,21 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern bool SetFocus(IntPtr hWnd); [DllImport("user32.dll")] + public static extern IntPtr GetFocus(); + [DllImport("user32.dll")] public static extern bool SetParent(IntPtr hWnd, IntPtr hWndNewParent); [DllImport("user32.dll")] public static extern IntPtr GetParent(IntPtr hWnd); + + public enum GetAncestorFlags + { + GA_PARENT = 1, + GA_ROOT = 2, + GA_ROOTOWNER = 3 + } + + [DllImport("user32.dll")] + public static extern IntPtr GetAncestor(IntPtr hwnd, GetAncestorFlags gaFlags); [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, ShowWindowCommand nCmdShow); @@ -1382,6 +1395,22 @@ namespace Avalonia.Win32.Interop throw new Exception("RtlGetVersion failed!"); } } + + [DllImport("kernel32", EntryPoint="WaitForMultipleObjectsEx", SetLastError = true, CharSet = CharSet.Auto)] + private static extern int IntWaitForMultipleObjectsEx(int nCount, IntPtr[] pHandles, bool bWaitAll, int dwMilliseconds, bool bAlertable); + + public const int WAIT_FAILED = unchecked((int)0xFFFFFFFF); + + internal static int WaitForMultipleObjectsEx(int nCount, IntPtr[] pHandles, bool bWaitAll, int dwMilliseconds, bool bAlertable) + { + int result = IntWaitForMultipleObjectsEx(nCount, pHandles, bWaitAll, dwMilliseconds, bAlertable); + if(result == WAIT_FAILED) + { + throw new Win32Exception(); + } + + return result; + } [DllImport("user32.dll")] internal static extern int SetWindowCompositionAttribute(IntPtr hwnd, ref WindowCompositionAttributeData data); diff --git a/src/Windows/Avalonia.Win32/NonPumpingWaitProvider.cs b/src/Windows/Avalonia.Win32/NonPumpingWaitProvider.cs new file mode 100644 index 0000000000..a0160fcfbd --- /dev/null +++ b/src/Windows/Avalonia.Win32/NonPumpingWaitProvider.cs @@ -0,0 +1,15 @@ +using System; +using Avalonia.Threading; +using Avalonia.Win32.Interop; + +namespace Avalonia.Win32 +{ + internal class NonPumpingWaitProvider : AvaloniaSynchronizationContext.INonPumpingPlatformWaitProvider + { + public int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout) + { + return UnmanagedMethods.WaitForMultipleObjectsEx(waitHandles.Length, waitHandles, waitAll, + millisecondsTimeout, false); + } + } +} diff --git a/src/Windows/Avalonia.Win32/PopupImpl.cs b/src/Windows/Avalonia.Win32/PopupImpl.cs index cd25b32ed9..57da1c4d66 100644 --- a/src/Windows/Avalonia.Win32/PopupImpl.cs +++ b/src/Windows/Avalonia.Win32/PopupImpl.cs @@ -7,6 +7,7 @@ namespace Avalonia.Win32 { class PopupImpl : WindowImpl, IPopupImpl { + private readonly IWindowBaseImpl _parent; private bool _dropShadowHint = true; private Size? _maxAutoSize; @@ -19,18 +20,25 @@ namespace Avalonia.Win32 public override void Show() { UnmanagedMethods.ShowWindow(Handle.Handle, UnmanagedMethods.ShowWindowCommand.ShowNoActivate); - var parent = UnmanagedMethods.GetParent(Handle.Handle); - if (parent != IntPtr.Zero) - { - IntPtr nextParent = parent; - while (nextParent != IntPtr.Zero) - { - parent = nextParent; - nextParent = UnmanagedMethods.GetParent(parent); - } - UnmanagedMethods.SetFocus(parent); + // We need to steal focus if it's held by a child window of our toplevel window + var parent = _parent; + while(parent != null) + { + if(parent is PopupImpl pi) + parent = pi._parent; + else + break; } + + if(parent == null) + return; + + var focusOwner = UnmanagedMethods.GetFocus(); + if (focusOwner != IntPtr.Zero && + UnmanagedMethods.GetAncestor(focusOwner, UnmanagedMethods.GetAncestorFlags.GA_ROOT) + == parent.Handle.Handle) + UnmanagedMethods.SetFocus(parent.Handle.Handle); } protected override bool ShouldTakeFocusOnClick => false; @@ -49,7 +57,7 @@ namespace Avalonia.Win32 { var info = UnmanagedMethods.MONITORINFO.Create(); UnmanagedMethods.GetMonitorInfo(monitor, ref info); - _maxAutoSize = info.rcWork.ToPixelRect().ToRect(Scaling).Size; + _maxAutoSize = info.rcWork.ToPixelRect().ToRect(RenderScaling).Size; } } @@ -118,6 +126,7 @@ namespace Avalonia.Win32 private PopupImpl(IWindowBaseImpl parent, bool dummy) : base() { + _parent = parent; PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, MoveResize)); } diff --git a/src/Windows/Avalonia.Win32/Win32NativeControlHost.cs b/src/Windows/Avalonia.Win32/Win32NativeControlHost.cs index d7bb2c037e..8f62163d81 100644 --- a/src/Windows/Avalonia.Win32/Win32NativeControlHost.cs +++ b/src/Windows/Avalonia.Win32/Win32NativeControlHost.cs @@ -176,7 +176,7 @@ namespace Avalonia.Win32 UnmanagedMethods.SetWindowPosFlags.SWP_NOACTIVATE); if (_attachedTo == null || _child == null) return; - size *= _attachedTo.Window.Scaling; + size *= _attachedTo.Window.RenderScaling; UnmanagedMethods.MoveWindow(_child.Handle, 0, 0, Math.Max(1, (int)size.Width), Math.Max(1, (int)size.Height), false); } @@ -186,7 +186,7 @@ namespace Avalonia.Win32 CheckDisposed(); if (_attachedTo == null) throw new InvalidOperationException("The control isn't currently attached to a toplevel"); - bounds *= _attachedTo.Window.Scaling; + bounds *= _attachedTo.Window.RenderScaling; var pixelRect = new PixelRect((int)bounds.X, (int)bounds.Y, Math.Max(1, (int)bounds.Width), Math.Max(1, (int)bounds.Height)); diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index b7bb0e19ba..af6058d197 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -93,6 +93,7 @@ namespace Avalonia.Win32 .Bind().ToConstant(s_instance) .Bind().ToSingleton() .Bind().ToConstant(s_instance) + .Bind().ToConstant(new NonPumpingWaitProvider()) .Bind().ToConstant(new WindowsMountedVolumeInfoProvider()); if (options.AllowEglInitialization) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs index 0ba1d311bc..25a34561fc 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -343,7 +343,7 @@ namespace Avalonia.Win32 { if (BeginPaint(_hwnd, out PAINTSTRUCT ps) != IntPtr.Zero) { - var f = Scaling; + var f = RenderScaling; var r = ps.rcPaint; Paint?.Invoke(new Rect(r.left / f, r.top / f, (r.right - r.left) / f, (r.bottom - r.top) / f)); @@ -368,7 +368,7 @@ namespace Avalonia.Win32 size == SizeCommand.Maximized)) { var clientSize = new Size(ToInt32(lParam) & 0xffff, ToInt32(lParam) >> 16); - Resized(clientSize / Scaling); + Resized(clientSize / RenderScaling); } var windowState = size == SizeCommand.Maximized ? @@ -402,29 +402,31 @@ namespace Avalonia.Win32 case WindowsMessage.WM_GETMINMAXINFO: { MINMAXINFO mmi = Marshal.PtrToStructure(lParam); + + _maxTrackSize = mmi.ptMaxTrackSize; if (_minSize.Width > 0) { mmi.ptMinTrackSize.X = - (int)((_minSize.Width * Scaling) + BorderThickness.Left + BorderThickness.Right); + (int)((_minSize.Width * RenderScaling) + BorderThickness.Left + BorderThickness.Right); } if (_minSize.Height > 0) { mmi.ptMinTrackSize.Y = - (int)((_minSize.Height * Scaling) + BorderThickness.Top + BorderThickness.Bottom); + (int)((_minSize.Height * RenderScaling) + BorderThickness.Top + BorderThickness.Bottom); } if (!double.IsInfinity(_maxSize.Width) && _maxSize.Width > 0) { mmi.ptMaxTrackSize.X = - (int)((_maxSize.Width * Scaling) + BorderThickness.Left + BorderThickness.Right); + (int)((_maxSize.Width * RenderScaling) + BorderThickness.Left + BorderThickness.Right); } if (!double.IsInfinity(_maxSize.Height) && _maxSize.Height > 0) { mmi.ptMaxTrackSize.Y = - (int)((_maxSize.Height * Scaling) + BorderThickness.Top + BorderThickness.Bottom); + (int)((_maxSize.Height * RenderScaling) + BorderThickness.Top + BorderThickness.Bottom); } Marshal.StructureToPtr(mmi, lParam, true); @@ -480,7 +482,7 @@ namespace Avalonia.Win32 private Point DipFromLParam(IntPtr lParam) { - return new Point((short)(ToInt32(lParam) & 0xffff), (short)(ToInt32(lParam) >> 16)) / Scaling; + return new Point((short)(ToInt32(lParam) & 0xffff), (short)(ToInt32(lParam) >> 16)) / RenderScaling; } private PixelPoint PointFromLParam(IntPtr lParam) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs index 2badf99f7f..a3b7574369 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs @@ -37,7 +37,7 @@ namespace Avalonia.Win32 if (_extendTitleBarHint >= 0) { - border_thickness.top = (int)(_extendedMargins.Top * Scaling); + border_thickness.top = (int)(_extendedMargins.Top * RenderScaling); } // Determine if the hit test is for resizing. Default middle (1,1). diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 0ee1342d27..6f22f94056 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -164,7 +164,9 @@ namespace Avalonia.Win32 } } - public double Scaling => _scaling; + public double RenderScaling => _scaling; + + public double DesktopScaling => RenderScaling; public Size ClientSize { @@ -172,7 +174,7 @@ namespace Avalonia.Win32 { GetClientRect(_hwnd, out var rect); - return new Size(rect.right, rect.bottom) / Scaling; + return new Size(rect.right, rect.bottom) / RenderScaling; } } @@ -180,7 +182,7 @@ namespace Avalonia.Win32 public IPlatformHandle Handle { get; private set; } - public virtual Size MaxAutoSizeHint => new Size(_maxTrackSize.X / Scaling, _maxTrackSize.Y / Scaling); + public virtual Size MaxAutoSizeHint => new Size(_maxTrackSize.X / RenderScaling, _maxTrackSize.Y / RenderScaling); public IMouseDevice MouseDevice => _mouseDevice; @@ -342,8 +344,8 @@ namespace Avalonia.Win32 public void Resize(Size value) { - int requestedClientWidth = (int)(value.Width * Scaling); - int requestedClientHeight = (int)(value.Height * Scaling); + int requestedClientWidth = (int)(value.Width * RenderScaling); + int requestedClientHeight = (int)(value.Height * RenderScaling); GetClientRect(_hwnd, out var clientRect); @@ -395,7 +397,7 @@ namespace Avalonia.Win32 public void Invalidate(Rect rect) { - var scaling = Scaling; + var scaling = RenderScaling; var r = new RECT { left = (int)Math.Floor(rect.X * scaling), @@ -411,12 +413,12 @@ namespace Avalonia.Win32 { var p = new POINT { X = point.X, Y = point.Y }; UnmanagedMethods.ScreenToClient(_hwnd, ref p); - return new Point(p.X, p.Y) / Scaling; + return new Point(p.X, p.Y) / RenderScaling; } public PixelPoint PointToScreen(Point point) { - point *= Scaling; + point *= RenderScaling; var p = new POINT { X = (int)point.X, Y = (int)point.Y }; ClientToScreen(_hwnd, ref p); return new PixelPoint(p.X, p.Y); @@ -710,19 +712,19 @@ namespace Avalonia.Win32 if (_extendTitleBarHint != -1) { - borderCaptionThickness.top = (int)(_extendTitleBarHint * Scaling); + borderCaptionThickness.top = (int)(_extendTitleBarHint * RenderScaling); } margins.cyTopHeight = _extendChromeHints.HasFlag(ExtendClientAreaChromeHints.SystemChrome) && !_extendChromeHints.HasFlag(ExtendClientAreaChromeHints.PreferSystemChrome) ? borderCaptionThickness.top : 1; if (WindowState == WindowState.Maximized) { - _extendedMargins = new Thickness(0, (borderCaptionThickness.top - borderThickness.top) / Scaling, 0, 0); - _offScreenMargin = new Thickness(borderThickness.left / Scaling, borderThickness.top / Scaling, borderThickness.right / Scaling, borderThickness.bottom / Scaling); + _extendedMargins = new Thickness(0, (borderCaptionThickness.top - borderThickness.top) / RenderScaling, 0, 0); + _offScreenMargin = new Thickness(borderThickness.left / RenderScaling, borderThickness.top / RenderScaling, borderThickness.right / RenderScaling, borderThickness.bottom / RenderScaling); } else { - _extendedMargins = new Thickness(0, (borderCaptionThickness.top) / Scaling, 0, 0); + _extendedMargins = new Thickness(0, (borderCaptionThickness.top) / RenderScaling, 0, 0); _offScreenMargin = new Thickness(); } @@ -1034,6 +1036,8 @@ namespace Avalonia.Win32 } } + double EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Scaling => RenderScaling; + IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => Handle.Handle; public void SetExtendClientAreaToDecorationsHint(bool hint) diff --git a/src/iOS/Avalonia.iOS/TopLevelImpl.cs b/src/iOS/Avalonia.iOS/TopLevelImpl.cs index 83a68990d7..5a85a5ea88 100644 --- a/src/iOS/Avalonia.iOS/TopLevelImpl.cs +++ b/src/iOS/Avalonia.iOS/TopLevelImpl.cs @@ -48,7 +48,7 @@ namespace Avalonia.iOS public new IPlatformHandle Handle => null; - public double Scaling => UIScreen.MainScreen.Scale; + public double RenderScaling => UIScreen.MainScreen.Scale; public override void LayoutSubviews() => Resized?.Invoke(ClientSize); diff --git a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj index 1c7077870a..51d18e55d1 100644 --- a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj +++ b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj @@ -18,6 +18,10 @@ - + + + + + diff --git a/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs b/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs new file mode 100644 index 0000000000..7af29a56a1 --- /dev/null +++ b/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs @@ -0,0 +1,16 @@ +using System; +using System.IO; +using System.Reflection; +using Avalonia.Markup.Xaml; +using Avalonia.Markup.Xaml.XamlIl; + +namespace Avalonia.Designer.HostApp +{ + class DesignXamlLoader : AvaloniaXamlLoader.IRuntimeXamlLoader + { + public object Load(Stream stream, Assembly localAsm, object o, Uri baseUri, bool designMode) + { + return AvaloniaXamlIlRuntimeCompiler.Load(stream, localAsm, o, baseUri, designMode); + } + } +} diff --git a/src/tools/Avalonia.Designer.HostApp/Program.cs b/src/tools/Avalonia.Designer.HostApp/Program.cs index 3163e1fbc3..4472dac4e3 100644 --- a/src/tools/Avalonia.Designer.HostApp/Program.cs +++ b/src/tools/Avalonia.Designer.HostApp/Program.cs @@ -1,6 +1,8 @@ using System; using System.IO; using System.Reflection; +using Avalonia.DesignerSupport; +using Avalonia.Markup.Xaml; namespace Avalonia.Designer.HostApp { @@ -40,8 +42,9 @@ namespace Avalonia.Designer.HostApp public static void Main(string[] args) #endif { + AvaloniaLocator.CurrentMutable.Bind() + .ToConstant(new DesignXamlLoader()); Avalonia.DesignerSupport.Remote.RemoteDesignerEntryPoint.Main(args); } - } } diff --git a/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs b/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs index e533001242..58ddc8ca60 100644 --- a/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs @@ -1,6 +1,5 @@ using System; -using System.Collections.Generic; -using Avalonia.Threading; +using Avalonia.Data; using Avalonia.UnitTests; using Xunit; @@ -32,5 +31,20 @@ namespace Avalonia.Controls.UnitTests Assert.True(raised); } } + + [Fact] + public void Can_Bind_To_DataContext() + { + using (UnitTestApplication.Start()) + { + var application = Application.Current; + + application.DataContext = "Test"; + + application.Bind(Application.NameProperty, new Binding(".")); + + Assert.Equal("Test", Application.Current.Name); + } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj index 2ca93dcf56..19c4454d3d 100644 --- a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj +++ b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj @@ -13,6 +13,7 @@ + diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs index 9a81d19bb9..7a2109e5a7 100644 --- a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs @@ -191,8 +191,7 @@ namespace Avalonia.Controls.UnitTests "; - var loader = new AvaloniaXamlLoader(); - var window = (Window)loader.Load(xaml); + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); var target1 = window.Find("target1"); var target2 = window.Find("target2"); var mouse = new MouseTestHelper(); @@ -235,8 +234,7 @@ namespace Avalonia.Controls.UnitTests "; - var loader = new AvaloniaXamlLoader(); - var window = (Window)loader.Load(xaml); + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); var target1 = window.Find("target1"); var target2 = window.Find("target2"); var mouse = new MouseTestHelper(); @@ -298,7 +296,7 @@ namespace Avalonia.Controls.UnitTests var windowImpl = MockWindowingPlatform.CreateWindowMock(); popupImpl = MockWindowingPlatform.CreatePopupMock(windowImpl.Object); - popupImpl.SetupGet(x => x.Scaling).Returns(1); + popupImpl.SetupGet(x => x.RenderScaling).Returns(1); windowImpl.Setup(x => x.CreatePopup()).Returns(popupImpl.Object); windowImpl.Setup(x => x.Screen).Returns(screenImpl.Object); diff --git a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs index dee7a84812..84f02aeda5 100644 --- a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs @@ -191,7 +191,8 @@ namespace Avalonia.Controls.UnitTests { var windowImpl = new Mock(); windowImpl.SetupProperty(x => x.Closed); - windowImpl.Setup(x => x.Scaling).Returns(1); + windowImpl.Setup(x => x.DesktopScaling).Returns(1); + windowImpl.Setup(x => x.RenderScaling).Returns(1); var services = TestServices.StyledWindow.With( windowingPlatform: new MockWindowingPlatform(() => windowImpl.Object)); diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs index fddc02f19c..fab57cec49 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs @@ -60,6 +60,25 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.IsType(target.Panel.Children[1]); } + [Fact] + public void Should_Create_Containers_Only_Once() + { + var parent = new TestItemsControl(); + var target = new ItemsPresenter + { + Items = new[] { "foo", "bar" }, + [StyledElement.TemplatedParentProperty] = parent, + }; + var raised = 0; + + parent.ItemContainerGenerator.Materialized += (s, e) => ++raised; + + target.ApplyTemplate(); + + Assert.Equal(2, target.Panel.Children.Count); + Assert.Equal(2, raised); + } + [Fact] public void ItemContainerGenerator_Should_Be_Picked_Up_From_TemplatedControl() { diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index fe9c7b1261..9ef2750ff3 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -531,6 +531,7 @@ namespace Avalonia.Controls.UnitTests.Primitives }; target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); target.SelectedIndex = 1; Assert.Equal(items[1], target.SelectedItem); @@ -549,6 +550,45 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.NotNull(receivedArgs); Assert.Empty(receivedArgs.AddedItems); Assert.Equal(new[] { removed }, receivedArgs.RemovedItems); + Assert.False(items.Single().IsSelected); + } + + [Fact] + public void Removing_Selected_Item_Should_Clear_Selection_With_BeginInit() + { + var items = new AvaloniaList + { + new Item(), + new Item(), + }; + + var target = new SelectingItemsControl(); + target.BeginInit(); + target.Items = items; + target.Template = Template(); + target.EndInit(); + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + target.SelectedIndex = 0; + + Assert.Equal(items[0], target.SelectedItem); + Assert.Equal(0, target.SelectedIndex); + + SelectionChangedEventArgs receivedArgs = null; + + target.SelectionChanged += (_, args) => receivedArgs = args; + + var removed = items[0]; + + items.RemoveAt(0); + + Assert.Null(target.SelectedItem); + Assert.Equal(-1, target.SelectedIndex); + Assert.NotNull(receivedArgs); + Assert.Empty(receivedArgs.AddedItems); + Assert.Equal(new[] { removed }, receivedArgs.RemovedItems); + Assert.False(items.Single().IsSelected); } [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/RelativePanelTests.cs b/tests/Avalonia.Controls.UnitTests/RelativePanelTests.cs index 4248e643eb..6e171a58e7 100644 --- a/tests/Avalonia.Controls.UnitTests/RelativePanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/RelativePanelTests.cs @@ -31,6 +31,7 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Rect(20, 0, 20, 20), target.Children[1].Bounds); } + [Fact] public void Lays_Out_1_Child_Below_the_other() { var rect1 = new Rectangle { Height = 20, Width = 20 }; @@ -55,5 +56,81 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Rect(0, 0, 20, 20), target.Children[0].Bounds); Assert.Equal(new Rect(0, 20, 20, 20), target.Children[1].Bounds); } + + [Fact] + public void RelativePanel_Can_Center() + { + var rect1 = new Rectangle { Height = 20, Width = 20 }; + var rect2 = new Rectangle { Height = 20, Width = 20 }; + + var target = new RelativePanel + { + VerticalAlignment = Layout.VerticalAlignment.Center, + HorizontalAlignment = Layout.HorizontalAlignment.Center, + Children = + { + rect1, rect2 + } + }; + + RelativePanel.SetAlignLeftWithPanel(rect1, true); + RelativePanel.SetBelow(rect2, rect1); + target.Measure(new Size(400, 400)); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(new Size(20, 40), target.Bounds.Size); + Assert.Equal(new Rect(0, 0, 20, 20), target.Children[0].Bounds); + Assert.Equal(new Rect(0, 20, 20, 20), target.Children[1].Bounds); + } + + [Fact] + public void LeftOf_Measures_Correctly() + { + var rect1 = new Rectangle { Height = 20, Width = 20 }; + var rect2 = new Rectangle { Height = 20, Width = 20 }; + + var target = new RelativePanel + { + VerticalAlignment = Layout.VerticalAlignment.Center, + HorizontalAlignment = Layout.HorizontalAlignment.Center, + Children = + { + rect1, rect2 + } + }; + + RelativePanel.SetLeftOf(rect2, rect1); + target.Measure(new Size(400, 400)); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(new Size(20, 20), target.Bounds.Size); + Assert.Equal(new Rect(0, 0, 20, 20), target.Children[0].Bounds); + Assert.Equal(new Rect(-20, 0, 20, 20), target.Children[1].Bounds); + } + + [Fact] + public void Above_Measures_Correctly() + { + var rect1 = new Rectangle { Height = 20, Width = 20 }; + var rect2 = new Rectangle { Height = 20, Width = 20 }; + + var target = new RelativePanel + { + VerticalAlignment = Layout.VerticalAlignment.Center, + HorizontalAlignment = Layout.HorizontalAlignment.Center, + Children = + { + rect1, rect2 + } + }; + + RelativePanel.SetAbove(rect2, rect1); + target.Measure(new Size(400, 400)); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(new Size(20, 20), target.Bounds.Size); + Assert.Equal(new Rect(0, 0, 20, 20), target.Children[0].Bounds); + Assert.Equal(new Rect(0, -20, 20, 20), target.Children[1].Bounds); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index db9211ac3c..fd52aeb9af 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -8,6 +8,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Controls.Utils; using Avalonia.LogicalTree; +using Avalonia.Markup.Xaml; using Avalonia.Styling; using Avalonia.UnitTests; using Xunit; @@ -338,8 +339,7 @@ namespace Avalonia.Controls.UnitTests xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'> "; - var loader = new Markup.Xaml.AvaloniaXamlLoader(); - var window = (Window)loader.Load(xaml); + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); var tabControl = window.FindControl("tabs"); tabControl.DataContext = new { Tabs = new List() }; diff --git a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs index 34b37e7635..9d7bc6af74 100644 --- a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs @@ -30,6 +30,40 @@ namespace Avalonia.Controls.UnitTests Assert.False(ToolTip.GetIsOpen(control)); } + + [Fact] + public void Should_Close_When_Control_Detaches() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = new Window(); + + var panel = new Panel(); + + var target = new Decorator() + { + [ToolTip.TipProperty] = "Tip", + [ToolTip.ShowDelayProperty] = 0 + }; + + panel.Children.Add(target); + + window.Content = panel; + + window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); + + Assert.True((target as IVisual).IsAttachedToVisualTree); + + _mouseHelper.Enter(target); + + Assert.True(ToolTip.GetIsOpen(target)); + + panel.Children.Remove(target); + + Assert.False(ToolTip.GetIsOpen(target)); + } + } [Fact] public void Should_Open_On_Pointer_Enter() diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index e49e273bec..6b30aed257 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -93,7 +93,7 @@ namespace Avalonia.Controls.UnitTests { var impl = new Mock(); impl.SetupProperty(x => x.Resized); - impl.SetupGet(x => x.Scaling).Returns(1); + impl.SetupGet(x => x.RenderScaling).Returns(1); var target = new TestTopLevel(impl.Object) { @@ -290,7 +290,7 @@ namespace Avalonia.Controls.UnitTests using (UnitTestApplication.Start(TestServices.StyledWindow)) { var impl = new Mock(); - impl.SetupGet(x => x.Scaling).Returns(1); + impl.SetupGet(x => x.RenderScaling).Returns(1); var child = new Border { Classes = { "foo" } }; var target = new TestTopLevel(impl.Object) diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index c1bd45bcad..c25ad19027 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -1273,8 +1273,6 @@ namespace Avalonia.Controls.UnitTests return new TextBlock { Text = node.Value }; } - public bool SupportsRecycling => false; - public InstancedBinding ItemsSelector(object item) { var obs = ExpressionObserver.Create(item, o => (o as Node).Children); diff --git a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs index 697ea9cff8..84f212d1b3 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs @@ -110,7 +110,8 @@ namespace Avalonia.Controls.UnitTests public void IsVisible_Should_Be_False_Atfer_Impl_Signals_Close() { var windowImpl = new Mock(); - windowImpl.Setup(x => x.Scaling).Returns(1); + windowImpl.Setup(x => x.DesktopScaling).Returns(1); + windowImpl.Setup(x => x.RenderScaling).Returns(1); windowImpl.SetupProperty(x => x.Closed); using (UnitTestApplication.Start(TestServices.StyledWindow)) @@ -128,7 +129,8 @@ namespace Avalonia.Controls.UnitTests public void Setting_IsVisible_True_Shows_Window() { var windowImpl = new Mock(); - windowImpl.Setup(x => x.Scaling).Returns(1); + windowImpl.Setup(x => x.DesktopScaling).Returns(1); + windowImpl.Setup(x => x.RenderScaling).Returns(1); using (UnitTestApplication.Start(TestServices.StyledWindow)) { @@ -143,7 +145,8 @@ namespace Avalonia.Controls.UnitTests public void Setting_IsVisible_False_Hides_Window() { var windowImpl = new Mock(); - windowImpl.Setup(x => x.Scaling).Returns(1); + windowImpl.Setup(x => x.DesktopScaling).Returns(1); + windowImpl.Setup(x => x.RenderScaling).Returns(1); using (UnitTestApplication.Start(TestServices.StyledWindow)) { @@ -208,7 +211,8 @@ namespace Avalonia.Controls.UnitTests { var renderer = new Mock(); var windowImpl = new Mock(); - windowImpl.Setup(x => x.Scaling).Returns(1); + windowImpl.Setup(x => x.DesktopScaling).Returns(1); + windowImpl.Setup(x => x.RenderScaling).Returns(1); windowImpl.SetupProperty(x => x.Closed); windowImpl.Setup(x => x.CreateRenderer(It.IsAny())).Returns(renderer.Object); @@ -237,7 +241,7 @@ namespace Avalonia.Controls.UnitTests public TestWindowBase(IRenderer renderer = null) : base(Mock.Of(x => - x.Scaling == 1 && + x.RenderScaling == 1 && x.CreateRenderer(It.IsAny()) == renderer)) { } diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index e2b0def00b..ba29001cf3 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -100,7 +100,8 @@ namespace Avalonia.Controls.UnitTests { var windowImpl = new Mock(); windowImpl.SetupProperty(x => x.Closed); - windowImpl.Setup(x => x.Scaling).Returns(1); + windowImpl.Setup(x => x.DesktopScaling).Returns(1); + windowImpl.Setup(x => x.RenderScaling).Returns(1); var services = TestServices.StyledWindow.With( windowingPlatform: new MockWindowingPlatform(() => windowImpl.Object)); @@ -206,7 +207,8 @@ namespace Avalonia.Controls.UnitTests var parent = new Mock(); var windowImpl = new Mock(); windowImpl.SetupProperty(x => x.Closed); - windowImpl.Setup(x => x.Scaling).Returns(1); + windowImpl.Setup(x => x.DesktopScaling).Returns(1); + windowImpl.Setup(x => x.RenderScaling).Returns(1); var target = new Window(windowImpl.Object); var task = target.ShowDialog(parent.Object); @@ -245,7 +247,8 @@ namespace Avalonia.Controls.UnitTests var parent = new Mock(); var windowImpl = new Mock(); windowImpl.SetupProperty(x => x.Closed); - windowImpl.Setup(x => x.Scaling).Returns(1); + windowImpl.Setup(x => x.DesktopScaling).Returns(1); + windowImpl.Setup(x => x.RenderScaling).Returns(1); var target = new Window(windowImpl.Object); var task = target.ShowDialog(parent.Object); @@ -273,7 +276,8 @@ namespace Avalonia.Controls.UnitTests var windowImpl = MockWindowingPlatform.CreateWindowMock(); windowImpl.Setup(x => x.ClientSize).Returns(new Size(800, 480)); - windowImpl.Setup(x => x.Scaling).Returns(1); + windowImpl.Setup(x => x.DesktopScaling).Returns(1); + windowImpl.Setup(x => x.RenderScaling).Returns(1); windowImpl.Setup(x => x.Screen).Returns(screens.Object); using (UnitTestApplication.Start(TestServices.StyledWindow)) @@ -298,12 +302,14 @@ namespace Avalonia.Controls.UnitTests var parentWindowImpl = MockWindowingPlatform.CreateWindowMock(); parentWindowImpl.Setup(x => x.ClientSize).Returns(new Size(800, 480)); parentWindowImpl.Setup(x => x.MaxAutoSizeHint).Returns(new Size(1920, 1080)); - parentWindowImpl.Setup(x => x.Scaling).Returns(1); + parentWindowImpl.Setup(x => x.DesktopScaling).Returns(1); + parentWindowImpl.Setup(x => x.RenderScaling).Returns(1); var windowImpl = MockWindowingPlatform.CreateWindowMock(); windowImpl.Setup(x => x.ClientSize).Returns(new Size(320, 200)); windowImpl.Setup(x => x.MaxAutoSizeHint).Returns(new Size(1920, 1080)); - windowImpl.Setup(x => x.Scaling).Returns(1); + windowImpl.Setup(x => x.DesktopScaling).Returns(1); + windowImpl.Setup(x => x.RenderScaling).Returns(1); var parentWindowServices = TestServices.StyledWindow.With( windowingPlatform: new MockWindowingPlatform(() => parentWindowImpl.Object)); @@ -565,7 +571,7 @@ namespace Avalonia.Controls.UnitTests private IWindowImpl CreateImpl(Mock renderer) { return Mock.Of(x => - x.Scaling == 1 && + x.RenderScaling == 1 && x.CreateRenderer(It.IsAny()) == renderer.Object); } diff --git a/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs b/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs index 25e8c82b1a..bf1322afbc 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs @@ -17,7 +17,7 @@ namespace Avalonia.Controls.UnitTests public IWindowImpl CreateWindow() { - return _windowImpl?.Invoke() ?? Mock.Of(x => x.Scaling == 1); + return _windowImpl?.Invoke() ?? Mock.Of(x => x.RenderScaling == 1); } public IWindowImpl CreateEmbeddableWindow() @@ -25,6 +25,6 @@ namespace Avalonia.Controls.UnitTests throw new NotImplementedException(); } - public IPopupImpl CreatePopup() => _popupImpl?.Invoke() ?? Mock.Of(x => x.Scaling == 1); + public IPopupImpl CreatePopup() => _popupImpl?.Invoke() ?? Mock.Of(x => x.RenderScaling == 1); } } diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index 00ef503b8d..530b8fa20c 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -355,7 +355,7 @@ namespace Avalonia.LeakTests var renderer = new Mock(); renderer.Setup(x => x.Dispose()); var impl = new Mock(); - impl.SetupGet(x => x.Scaling).Returns(1); + impl.SetupGet(x => x.RenderScaling).Returns(1); impl.SetupProperty(x => x.Closed); impl.Setup(x => x.CreateRenderer(It.IsAny())).Returns(renderer.Object); impl.Setup(x => x.Dispose()).Callback(() => impl.Object.Closed()); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj index e8c4daa7bc..ad3592294d 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj @@ -11,6 +11,7 @@ + diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ConverterTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ConverterTests.cs index b424003ed6..c9420f1696 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ConverterTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ConverterTests.cs @@ -9,7 +9,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters public void Bug_2228_Relative_Uris_Should_Be_Correctly_Parsed() { var testClass = typeof(TestClassWithUri); - var parsed = AvaloniaXamlLoader.Parse( + var parsed = AvaloniaRuntimeXamlLoader.Parse( $"<{testClass.Name} xmlns='clr-namespace:{testClass.Namespace}' Uri='/test'/>", testClass.Assembly); Assert.False(parsed.Uri.IsAbsoluteUri); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/MultiValueConverterTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/MultiValueConverterTests.cs index a77723afe1..466ae1bf7c 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/MultiValueConverterTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/MultiValueConverterTests.cs @@ -29,8 +29,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters "; - var loader = new AvaloniaXamlLoader(); - var window = (Window)loader.Load(xaml); + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); var textBlock = window.FindControl("textBlock"); window.ApplyTemplate(); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/NullableConverterTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/NullableConverterTests.cs index cdd40ed80f..eb8851c80b 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/NullableConverterTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/NullableConverterTests.cs @@ -22,8 +22,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters xmlns='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Converters' Thickness = '5' Orientation='Vertical' >"; - var loader = new AvaloniaXamlLoader(); - var data = (ClassWithNullableProperties)loader.Load(xaml, typeof(ClassWithNullableProperties).Assembly); + var data = (ClassWithNullableProperties)AvaloniaRuntimeXamlLoader.Load(xaml, typeof(ClassWithNullableProperties).Assembly); Assert.Equal(new Thickness(5), data.Thickness); Assert.Equal(Orientation.Vertical, data.Orientation); } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/PointsListTypeConverterTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/PointsListTypeConverterTests.cs index b060905f38..3b729e9cd8 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/PointsListTypeConverterTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/PointsListTypeConverterTests.cs @@ -31,8 +31,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters public void Should_Parse_Points_in_Xaml(string input) { var xaml = $""; - var loader = new AvaloniaXamlLoader(); - var polygon = (Polygon)loader.Load(xaml); + var polygon = (Polygon)AvaloniaRuntimeXamlLoader.Load(xaml); var points = polygon.Points; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ValueConverterTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ValueConverterTests.cs index 5e698117c3..4d5983e276 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ValueConverterTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ValueConverterTests.cs @@ -21,8 +21,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters xmlns:c='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Converters;assembly=Avalonia.Markup.Xaml.UnitTests'> "; - var loader = new AvaloniaXamlLoader(); - var window = (Window)loader.Load(xaml); + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); var textBlock = window.FindControl("textBlock"); window.ApplyTemplate(); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs index 6730e3134d..afc4a36fea 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs @@ -24,8 +24,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'> "; - var loader = new AvaloniaXamlLoader(); - var window = (Window)loader.Load(xaml); + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); var textBlock = window.FindControl("textBlock"); window.DataContext = "foo"; @@ -45,8 +44,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'> "; - var loader = new AvaloniaXamlLoader(); - var window = (Window)loader.Load(xaml); + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); var textBlock = window.FindControl("textBlock"); window.ApplyTemplate(); @@ -86,8 +84,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data "; - var loader = new AvaloniaXamlLoader(); - var window = (Window)loader.Load(xaml); + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); var textBox = window.FindControl("textBox"); window.ApplyTemplate(); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs index b2b4c4da1a..a7a004bd49 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs @@ -20,8 +20,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'> "; - var loader = new AvaloniaXamlLoader(); - var window = (Window)loader.Load(xaml); + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); var button = window.FindControl "; - var control = AvaloniaXamlLoader.Parse(xaml); + var control = AvaloniaRuntimeXamlLoader.Parse(xaml); var button = control.FindControl "; - var control = AvaloniaXamlLoader.Parse(xaml); + var control = AvaloniaRuntimeXamlLoader.Parse(xaml); var button = control.FindControl