Browse Source

Merge branch 'master' into master

pull/4357/head
Benedikt Stebner 6 years ago
committed by GitHub
parent
commit
c7066078be
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .gitmodules
  2. 39
      Avalonia.sln
  3. 1
      Avalonia.v3.ncrunchsolution
  4. 1
      Directory.Build.props
  5. 2
      dirs.proj
  6. 16
      native/Avalonia.Native/src/OSX/Screens.mm
  7. 13
      native/Avalonia.Native/src/OSX/window.mm
  8. 24
      samples/ControlCatalog/App.xaml.cs
  9. 1
      samples/ControlCatalog/MainView.xaml
  10. 42
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
  11. 6
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs
  12. 8
      samples/ControlCatalog/Pages/ListBoxPage.xaml
  13. 31
      samples/ControlCatalog/Pages/MenuPage.xaml
  14. 10
      samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs
  15. 36
      samples/ControlCatalog/ViewModels/MenuPageViewModel.cs
  16. 5
      samples/RenderDemo/App.xaml
  17. 125
      samples/RenderDemo/SideBar.xaml
  18. 2
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  19. 27
      src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs
  20. 10
      src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
  21. 6
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  22. 6
      src/Avalonia.Controls/Chrome/CaptionButtons.cs
  23. 126
      src/Avalonia.Controls/Chrome/TitleBar.cs
  24. 16
      src/Avalonia.Controls/DateTimePickers/DatePicker.cs
  25. 31
      src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs
  26. 51
      src/Avalonia.Controls/DateTimePickers/TimePicker.cs
  27. 38
      src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs
  28. 2
      src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs
  29. 1
      src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
  30. 6
      src/Avalonia.Controls/Platform/ITopLevelImpl.cs
  31. 5
      src/Avalonia.Controls/Platform/IWindowBaseImpl.cs
  32. 5
      src/Avalonia.Controls/Presenters/CarouselPresenter.cs
  33. 21
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  34. 2
      src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
  35. 7
      src/Avalonia.Controls/Primitives/ChromeOverlayLayer.cs
  36. 4
      src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs
  37. 20
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  38. 29
      src/Avalonia.Controls/Primitives/VisualLayerManager.cs
  39. 2
      src/Avalonia.Controls/RelativePanel.AttachedProperties.cs
  40. 487
      src/Avalonia.Controls/RelativePanel.cs
  41. 2
      src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs
  42. 27
      src/Avalonia.Controls/Repeater/ElementFactory.cs
  43. 66
      src/Avalonia.Controls/Repeater/IElementFactory.cs
  44. 19
      src/Avalonia.Controls/Repeater/ItemTemplateWrapper.cs
  45. 4
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  46. 12
      src/Avalonia.Controls/Repeater/RecyclePool.cs
  47. 119
      src/Avalonia.Controls/Repeater/RecyclingElementFactory.cs
  48. 31
      src/Avalonia.Controls/Repeater/ViewManager.cs
  49. 29
      src/Avalonia.Controls/Templates/FuncDataTemplate.cs
  50. 6
      src/Avalonia.Controls/Templates/FuncTemplate`2.cs
  51. 12
      src/Avalonia.Controls/Templates/IDataTemplate.cs
  52. 25
      src/Avalonia.Controls/Templates/IRecyclingDataTemplate.cs
  53. 2
      src/Avalonia.Controls/TextBox.cs
  54. 25
      src/Avalonia.Controls/TickBar.cs
  55. 8
      src/Avalonia.Controls/ToolTipService.cs
  56. 4
      src/Avalonia.Controls/TopLevel.cs
  57. 20
      src/Avalonia.Controls/Window.cs
  58. 7
      src/Avalonia.DesignerSupport/DesignWindowLoader.cs
  59. 10
      src/Avalonia.DesignerSupport/Remote/FileWatcherTransport.cs
  60. 28
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json
  61. 1
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
  62. 7
      src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs
  63. 3
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  64. 2
      src/Avalonia.Diagnostics/Diagnostics/ViewLocator.cs
  65. 2
      src/Avalonia.FreeDesktop/DBusHelper.cs
  66. 9
      src/Avalonia.Headless/HeadlessWindowImpl.cs
  67. 15
      src/Avalonia.Native/OsxManagedPopupPositionerPopupImplHelper.cs
  68. 5
      src/Avalonia.Native/OsxUnicodeKeys.cs
  69. 2
      src/Avalonia.Native/PopupImpl.cs
  70. 8
      src/Avalonia.Native/WindowImplBase.cs
  71. 2
      src/Avalonia.Themes.Default/TextBox.xaml
  72. 2
      src/Avalonia.Themes.Default/TitleBar.xaml
  73. 3
      src/Avalonia.Themes.Default/ToggleSwitch.xaml
  74. 34
      src/Avalonia.Themes.Default/Window.xaml
  75. 6
      src/Avalonia.Themes.Fluent/Menu.xaml
  76. 184
      src/Avalonia.Themes.Fluent/MenuItem.xaml
  77. 2
      src/Avalonia.Themes.Fluent/NumericUpDown.xaml
  78. 107
      src/Avalonia.Themes.Fluent/TextBox.xaml
  79. 2
      src/Avalonia.Themes.Fluent/TitleBar.xaml
  80. 3
      src/Avalonia.Themes.Fluent/ToggleSwitch.xaml
  81. 22
      src/Avalonia.Themes.Fluent/Window.xaml
  82. 15
      src/Avalonia.Visuals/Media/Color.cs
  83. 15
      src/Avalonia.Visuals/Media/TextFormatting/GenericTextParagraphProperties.cs
  84. 3
      src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs
  85. 23
      src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs
  86. 18
      src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingStyle.cs
  87. 503
      src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
  88. 67
      src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
  89. 45
      src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs
  90. 157
      src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs
  91. 11
      src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs
  92. 5
      src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs
  93. 7
      src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs
  94. 33
      src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs
  95. 37
      src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs
  96. 2
      src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs
  97. 1
      src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs
  98. 16
      src/Avalonia.Visuals/Media/TextWrapping.cs
  99. 3
      src/Avalonia.X11/NativeDialogs/Gtk.cs
  100. 45
      src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs

2
.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

39
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}

1
Avalonia.v3.ncrunchsolution

@ -3,6 +3,7 @@
<AdditionalFilesToIncludeForSolution>
<Value>tests\TestFiles\**.*</Value>
<Value>src\Avalonia.Build.Tasks\bin\Debug\netstandard2.0\Avalonia.Build.Tasks.dll</Value>
<Value>src\Avalonia.Build.Tasks\bin\Debug\netstandard2.0\Mono.Cecil.dll</Value>
</AdditionalFilesToIncludeForSolution>
<AllowParallelTestExecution>True</AllowParallelTestExecution>
<ProjectConfigStoragePathRelativeToSolutionDir>.ncrunch</ProjectConfigStoragePathRelativeToSolutionDir>

1
Directory.Build.props

@ -1,5 +1,6 @@
<Project>
<PropertyGroup>
<PackageOutputPath Condition="'$(PackageOutputPath)' == ''">$(MSBuildThisFileDirectory)build-intermediate/nuget</PackageOutputPath>
<AvaloniaPreviewerNetCoreToolPath>$(MSBuildThisFileDirectory)\src\tools\Avalonia.Designer.HostApp\bin\$(Configuration)\netcoreapp2.0\Avalonia.Designer.HostApp.dll</AvaloniaPreviewerNetCoreToolPath>
</PropertyGroup>
</Project>

2
dirs.proj

@ -6,7 +6,7 @@
<ProjectReference Include="packages/**/*.*proj" />
<ProjectReference Remove="**/*.shproj" />
<ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml/PortableXaml/**/*.*proj" />
<ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml/XamlIl/**/*.*proj" />
<ProjectReference Remove="src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github/**/*.*proj" />
</ItemGroup>
<!--<ItemGroup Condition="!Exists('$(MSBuildExtensionsPath)\Xamarin\Android')">-->
<ItemGroup>

16
native/Avalonia.Native/src/OSX/Screens.mm

@ -4,6 +4,14 @@ class Screens : public ComSingleObject<IAvnScreens, &IID_IAvnScreens>
{
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<IAvnScreens, &IID_IAvnScreens>
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];

13
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

24
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")
},
};

1
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}">
<Grid>

42
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml

@ -1,6 +1,30 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.ItemsRepeaterPage">
<UserControl.Resources>
<RecyclePool x:Key="RecyclePool" />
<DataTemplate x:Key="odd">
<TextBlock Background="Yellow"
Foreground="Black"
Height="{Binding Height}"
Text="{Binding Text}"/>
</DataTemplate>
<DataTemplate x:Key="even">
<TextBlock Background="Wheat"
Foreground="Black"
Height="{Binding Height}"
Text="{Binding Text}"/>
</DataTemplate>
<RecyclingElementFactory x:Key="elementFactory"
RecyclePool="{StaticResource RecyclePool}"
SelectTemplateKey="OnSelectTemplateKey">
<RecyclingElementFactory.Templates>
<StaticResource x:Key="odd" ResourceKey="odd" />
<StaticResource x:Key="even" ResourceKey="even" />
</RecyclingElementFactory.Templates>
</RecyclingElementFactory>
</UserControl.Resources>
<DockPanel>
<StackPanel DockPanel.Dock="Top" Spacing="4" Margin="0 0 0 16">
<TextBlock Classes="h1">ItemsRepeater</TextBlock>
@ -12,8 +36,6 @@
<ComboBoxItem>Stack - Horizontal</ComboBoxItem>
<ComboBoxItem>UniformGrid - Vertical</ComboBoxItem>
<ComboBoxItem>UniformGrid - Horizontal</ComboBoxItem>
<ComboBoxItem>WrapLayout - Horizontal</ComboBoxItem>
<ComboBoxItem>WrapLayout - Veritcal</ComboBoxItem>
</ComboBox>
<Button Command="{Binding AddItem}">Add Item</Button>
<Button Command="{Binding RandomizeHeights}">Randomize Heights</Button>
@ -25,20 +47,8 @@
<ScrollViewer Name="scroller"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<ItemsRepeater Name="repeater" Background="Transparent" Items="{Binding Items}">
<ItemsRepeater.Styles>
<Style Selector="Border:pointerover">
<Setter Property="Width" Value="200" />
</Style>
</ItemsRepeater.Styles>
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<Border Background="Purple" BorderThickness="2" BorderBrush="Green" MinHeight="100" MinWidth="100">
<TextBlock Focusable="True" Height="{Binding Height}" Text="{Binding Text}" Foreground="White" />
</Border>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
<ItemsRepeater Name="repeater" Background="Transparent" Items="{Binding Items}"
ItemTemplate="{StaticResource elementFactory}"/>
</ScrollViewer>
</Border>
</DockPanel>

6
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)

8
samples/ControlCatalog/Pages/ListBoxPage.xaml

@ -10,7 +10,13 @@
HorizontalAlignment="Center"
Spacing="16">
<StackPanel Orientation="Vertical" Spacing="8">
<ListBox Items="{Binding Items}" SelectedItem="{Binding SelectedItem}" AutoScrollToSelectedItem="True" SelectionMode="{Binding SelectionMode}" Width="250" Height="350"></ListBox>
<ListBox Items="{Binding Items}"
SelectedItem="{Binding SelectedItem}"
SelectedItems="{Binding SelectedItems}"
AutoScrollToSelectedItem="True"
SelectionMode="{Binding SelectionMode}"
Width="250"
Height="350"/>
<Button Command="{Binding AddItemCommand}">Add</Button>

31
samples/ControlCatalog/Pages/MenuPage.xaml

@ -20,7 +20,9 @@
<Separator/>
<MenuItem Header="Menu with _Submenu">
<MenuItem Header="Submenu _1"/>
<MenuItem Header="Submenu _2"/>
<MenuItem Header="Submenu _2 with Submenu">
<MenuItem Header="Submenu Level 2" />
</MenuItem>
</MenuItem>
<MenuItem Header="Menu Item with _Icon" InputGesture="Ctrl+Shift+B">
<MenuItem.Icon>
@ -52,6 +54,33 @@
</Menu.Styles>
</Menu>
</StackPanel>
<StackPanel>
<TextBlock Classes="h3" Margin="4 8">Mixed</TextBlock>
<Menu>
<MenuItem Header="_File">
<MenuItem Header="_New" CommandParameter="{Binding}" InputGesture="Ctrl+N"/>
<Separator/>
<MenuItem Header="_Open..." InputGesture="Ctrl+O"/>
<Separator/>
<MenuItem Header="Execu_te Script..." />
<Separator/>
<MenuItem Header="_Recent" Items="{Binding RecentItems}">
<MenuItem.Styles>
<Style Selector="MenuItem">
<Setter Property="Header" Value="{Binding Header}"/>
</Style>
</MenuItem.Styles>
</MenuItem>
<Separator/>
<MenuItem Header="E_xit" InputGesture="Alt+F4"/>
</MenuItem>
<MenuItem Header="_Help">
<MenuItem Header="_About"/>
</MenuItem>
</Menu>
</StackPanel>
</StackPanel>
</StackPanel>
</UserControl>

10
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;
}
}
}

36
samples/ControlCatalog/ViewModels/MenuPageViewModel.cs

@ -17,6 +17,23 @@ namespace ControlCatalog.ViewModels
SaveCommand = ReactiveCommand.Create(Save, Observable.Return(false));
OpenRecentCommand = ReactiveCommand.Create<string>(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<MenuItemViewModel> MenuItems { get; set; }
public IReadOnlyList<MenuItemViewModel> RecentItems { get; set; }
public ReactiveCommand<Unit, Unit> OpenCommand { get; }
public ReactiveCommand<Unit, Unit> SaveCommand { get; }
public ReactiveCommand<string, Unit> OpenRecentCommand { get; }

5
samples/RenderDemo/App.xaml

@ -3,8 +3,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="RenderDemo.App">
<Application.Styles>
<StyleInclude Source="avares://Avalonia.Themes.Default/DefaultTheme.xaml"/>
<StyleInclude Source="avares://Avalonia.Themes.Default/Accents/BaseLight.xaml"/>
<StyleInclude Source="avares://Avalonia.Themes.Fluent/Accents/FluentLight.xaml"/>
<StyleInclude Source="avares://RenderDemo/SideBar.xaml"/>
</Application.Styles>
</Application>
</Application>

125
samples/RenderDemo/SideBar.xaml

@ -1,65 +1,68 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style Selector="TabControl.sidebar">
<Setter Property="TabStripPlacement" Value="Left"/>
<Setter Property="Padding" Value="8 0 0 0"/>
<Setter Property="Background" Value="{DynamicResource SystemAccentColor}"/>
<Setter Property="Template">
<ControlTemplate>
<Border
Margin="{TemplateBinding Margin}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<DockPanel>
<ScrollViewer
Name="PART_ScrollViewer"
HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"
Background="{TemplateBinding Background}">
<ItemsPresenter
Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}">
</ItemsPresenter>
</ScrollViewer>
<ContentPresenter
Name="PART_SelectedContentHost"
Margin="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding SelectedContent}"
ContentTemplate="{TemplateBinding SelectedContentTemplate}">
</ContentPresenter>
</DockPanel>
</Border>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="TabControl.sidebar">
<Setter Property="TabStripPlacement" Value="Left"/>
<Setter Property="Padding" Value="8 0 0 0"/>
<Setter Property="Background" Value="{DynamicResource SystemAccentColor}"/>
<Setter Property="Template">
<ControlTemplate>
<Border
Margin="{TemplateBinding Margin}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<DockPanel>
<ScrollViewer
Name="PART_ScrollViewer"
HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}"
Background="{TemplateBinding Background}">
<ItemsPresenter
Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}">
</ItemsPresenter>
</ScrollViewer>
<ContentPresenter
Name="PART_SelectedContentHost"
Margin="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding SelectedContent}"
ContentTemplate="{TemplateBinding SelectedContentTemplate}">
</ContentPresenter>
</DockPanel>
</Border>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="TabControl.sidebar > TabItem">
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Margin" Value="0"/>
<Setter Property="Padding" Value="16"/>
<Setter Property="Opacity" Value="0.5"/>
<Setter Property="Transitions">
<Transitions>
<DoubleTransition Property="Opacity" Duration="0:0:0.150"/>
</Transitions>
</Setter>
</Style>
<Style Selector="TabControl.sidebar > TabItem:pointerover">
<Setter Property="Opacity" Value="1"/>
</Style>
<Style Selector="TabControl.sidebar > TabItem:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="Transparent"/>
</Style>
<Style Selector="TabControl.sidebar > TabItem:selected">
<Setter Property="Opacity" Value="1"/>
</Style>
<Style Selector="TabControl.sidebar > TabItem:selected /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource SystemAccentColorLight1}"/>
</Style>
<Style Selector="TabControl.sidebar > TabItem">
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="Margin" Value="0"/>
<Setter Property="Padding" Value="16"/>
<Setter Property="Opacity" Value="0.5"/>
<Setter Property="Transitions">
<Transitions>
<DoubleTransition Property="Opacity" Duration="0:0:0.150"/>
</Transitions>
</Setter>
</Style>
<Style Selector="TabControl.sidebar > TabItem:selected /template/ Border#PART_SelectedPipe">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="TabControl.sidebar > TabItem:pointerover">
<Setter Property="Opacity" Value="1"/>
</Style>
<Style Selector="TabControl.sidebar > TabItem:pointerover /template/ Border#PART_LayoutRoot">
<Setter Property="Background" Value="{DynamicResource SystemAccentColorLight2}"/>
</Style>
<Style Selector="TabControl.sidebar > TabItem:selected">
<Setter Property="Opacity" Value="1"/>
</Style>
<Style Selector="TabControl.sidebar > TabItem:selected /template/ Border#PART_LayoutRoot">
<Setter Property="Background" Value="{DynamicResource SystemAccentColorLight1}"/>
</Style>
</Styles>

2
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()
{

27
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
/// </summary>
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();
}
/// <summary>
/// Controls if SynchronizationContext should be installed in InstallIfNeeded. Used by Designer.
/// </summary>
@ -22,7 +38,8 @@ namespace Avalonia.Threading
return;
}
SetSynchronizationContext(new AvaloniaSynchronizationContext());
SetSynchronizationContext(new AvaloniaSynchronizationContext(AvaloniaLocator.Current
.GetService<INonPumpingPlatformWaitProvider>()));
}
/// <inheritdoc/>
@ -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);
}
}
}

10
src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj

@ -17,14 +17,14 @@
<Compile Include="../Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaResourceXamlInfo.cs">
<Link>Shared/AvaloniaResourceXamlInfo.cs</Link>
</Compile>
<Compile Include="../Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/**/*.cs">
<Compile Include="../Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/**/*.cs">
<Link>XamlIlExtensions/%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile>
<Compile Remove="external/cecil/**/*.*" />
<Compile Include="../Markup/Avalonia.Markup.Xaml/XamlIl\xamlil.github\src\XamlX\**\*.cs">
<Compile Include="../Markup/Avalonia.Markup.Xaml.Loader/xamlil.github\src\XamlX\**\*.cs">
<Link>XamlIl/%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile>
<Compile Include="../Markup/Avalonia.Markup.Xaml/XamlIl\xamlil.github\src\XamlX.IL.Cecil\**\*.cs">
<Compile Include="../Markup/Avalonia.Markup.Xaml.Loader\xamlil.github\src\XamlX.IL.Cecil\**\*.cs">
<Link>XamlIl.Cecil/%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile>
<Compile Include="../Markup/Avalonia.Markup\Markup\Parsers\SelectorGrammar.cs">
@ -57,8 +57,8 @@
<Compile Include="../Avalonia.Base/Utilities/StyleClassParser.cs">
<Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
</Compile>
<Compile Remove="../Markup/Avalonia.Markup.Xaml/XamlIl\xamlil.github\**\obj\**\*.cs" />
<Compile Remove="../Markup/Avalonia.Markup.Xaml/XamlIl\xamlil.github\src\XamlX\IL\SreTypeSystem.cs" />
<Compile Remove="../Markup/Avalonia.Markup.Xaml.Loader\xamlil.github\**\obj\**\*.cs" />
<Compile Remove="../Markup/Avalonia.Markup.Xaml.Loader\xamlil.github\src\XamlX\IL\SreTypeSystem.cs" />
<PackageReference Include="Avalonia.Unofficial.Cecil" Version="20190417.2.0" PrivateAssets="All" />
<PackageReference Include="Microsoft.Build.Framework" Version="15.1.548" PrivateAssets="All" />
</ItemGroup>

6
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

6
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;
}
}

126
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<CaptionButtons>("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<CaptionButtons>("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;
}
}
}

16
src/Avalonia.Controls/DateTimePickers/DatePicker.cs

@ -88,7 +88,7 @@ namespace Avalonia.Controls
AvaloniaProperty.RegisterDirect<DatePicker, DateTimeOffset?>(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);

31
src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs

@ -77,7 +77,7 @@ namespace Avalonia.Controls
DatePicker.YearVisibleProperty.AddOwner<DatePickerPresenter>(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<Grid>("PickerContainer");
_monthHost = e.NameScope.Get<Panel>("MonthHost");
_dayHost = e.NameScope.Get<Panel>("DayHost");
@ -326,7 +326,7 @@ namespace Avalonia.Controls
/// </summary>
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;

51
src/Avalonia.Controls/DateTimePickers/TimePicker.cs

@ -15,7 +15,7 @@ namespace Avalonia.Controls
/// Defines the <see cref="MinuteIncrement"/> property
/// </summary>
public static readonly DirectProperty<TimePicker, int> MinuteIncrementProperty =
AvaloniaProperty.RegisterDirect<TimePicker, int>(nameof(MinuteIncrement),
AvaloniaProperty.RegisterDirect<TimePicker, int>(nameof(MinuteIncrement),
x => x.MinuteIncrement, (x, v) => x.MinuteIncrement = v);
/// <summary>
@ -34,17 +34,17 @@ namespace Avalonia.Controls
/// Defines the <see cref="ClockIdentifier"/> property
/// </summary>
public static readonly DirectProperty<TimePicker, string> ClockIdentifierProperty =
AvaloniaProperty.RegisterDirect<TimePicker, string>(nameof(ClockIdentifier),
AvaloniaProperty.RegisterDirect<TimePicker, string>(nameof(ClockIdentifier),
x => x.ClockIdentifier, (x, v) => x.ClockIdentifier = v);
/// <summary>
/// Defines the <see cref="SelectedTime"/> property
/// </summary>
public static readonly DirectProperty<TimePicker, TimeSpan?> SelectedTimeProperty =
AvaloniaProperty.RegisterDirect<TimePicker, TimeSpan?>(nameof(SelectedTime),
AvaloniaProperty.RegisterDirect<TimePicker, TimeSpan?>(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>("Popup");
_presenter = e.NameScope.Find<TimePickerPresenter>("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;
}
}
}

38
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
/// </summary>
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();
}

2
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

1
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;
}

6
src/Avalonia.Controls/Platform/ITopLevelImpl.cs

@ -23,10 +23,10 @@ namespace Avalonia.Platform
Size ClientSize { get; }
/// <summary>
/// Gets the scaling factor for the toplevel.
/// Gets the scaling factor for the toplevel. This is used for rendering.
/// </summary>
double Scaling { get; }
double RenderScaling { get; }
/// <summary>
/// The list of native platform's surfaces that can be consumed by rendering subsystems.
/// </summary>

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

@ -13,6 +13,11 @@ namespace Avalonia.Platform
/// Hides the window.
/// </summary>
void Hide();
/// <summary>
/// Gets the scaling factor for Window positioning and sizing.
/// </summary>
double DesktopScaling { get; }
/// <summary>
/// Gets the position of the window in device pixels.

5
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));
}
/// <summary>
/// Moves to the selected page, animating if a <see cref="PageTransition"/> is set.
/// </summary>

21
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();
/// <summary>
@ -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();

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

@ -229,8 +229,6 @@ namespace Avalonia.Controls.Presenters
}
PanelCreated(Panel);
ItemsChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
/// <summary>

7
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);
}
}

4
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;
}
}

20
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;

29
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<Control> _layers = new List<Control>();
public static readonly StyledProperty<ChromeOverlayLayer> ChromeOverlayLayerProperty =
AvaloniaProperty.Register<VisualLayerManager, ChromeOverlayLayer>(nameof(ChromeOverlayLayer));
public bool IsPopup { get; set; }
public AdornerLayer AdornerLayer
{
get
@ -30,10 +33,19 @@ namespace Avalonia.Controls.Primitives
{
get
{
var rv = FindLayer<ChromeOverlayLayer>();
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<OverlayLayer>();
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();
}

2
src/Avalonia.Controls/RelativePanel.AttachedProperties.cs

@ -33,7 +33,7 @@ namespace Avalonia.Controls
AlignVerticalCenterWithProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged);
BelowProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged);
LeftOfProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged);
LeftOfProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged);
RightOfProperty.Changed.AddClassHandler<Layoutable>(OnAlignPropertiesChanged);
}
/// <summary>

487
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<Layoutable>())
_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<GraphNode> OutgoingNodes { get; }
@ -132,19 +116,106 @@ namespace Avalonia.Controls
OutgoingNodes = new HashSet<GraphNode>();
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<GraphNode> 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<AvaloniaObject, GraphNode> _nodeDic;
private Size _arrangeSize;
private Size AvailableSize { get; set; }
public Graph() => _nodeDic = new Dictionary<AvaloniaObject, GraphNode>();
public IEnumerable<GraphNode> GetNodes() => _nodeDic.Values;
public Graph()
public void Clear()
{
_nodeDic = new Dictionary<AvaloniaObject, GraphNode>();
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<GraphNode> nodes, HashSet<Layoutable>? set)
private void Measure(IEnumerable<GraphNode> nodes, HashSet<AvaloniaObject>? set)
{
set ??= new HashSet<Layoutable>();
set ??= new HashSet<AvaloniaObject>();
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
{
/// <summary>
/// Returns a value that indicates whether the specified value is not a number ().
/// </summary>
/// <param name="d">A double-precision floating-point number.</param>
/// <returns>true if evaluates to ; otherwise, false.</returns>
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<TSource> Do<TSource>(this IEnumerable<TSource> source, Action<TSource> predicate)
{
var enumerable = source as IList<TSource> ?? source.ToList();
foreach (var item in enumerable)
{
predicate.Invoke(item);
}
return enumerable;
}
}
}

2
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(() =>
{

27
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);
}
}

66
src/Avalonia.Controls/Repeater/IElementFactory.cs

@ -0,0 +1,66 @@
using Avalonia.Controls.Templates;
namespace Avalonia.Controls
{
/// <summary>
/// Represents the optional arguments to use when calling an implementation of the
/// <see cref="IElementFactory"/>'s <see cref="IElementFactory.GetElement"/> method.
/// </summary>
public class ElementFactoryGetArgs
{
/// <summary>
/// Gets or sets the data item for which an appropriate element tree should be realized
/// when calling <see cref="IElementFactory.GetElement"/>.
/// </summary>
public object Data { get; set; }
/// <summary>
/// Gets or sets the <see cref="IControl"/> that is expected to be the parent of the
/// realized element from <see cref="IElementFactory.GetElement"/>.
/// </summary>
public IControl Parent { get; set; }
/// <summary>
/// Gets or sets the index of the item that should be realized.
/// </summary>
public int Index { get; set; }
}
/// <summary>
/// Represents the optional arguments to use when calling an implementation of the
/// <see cref="IElementFactory"/>'s <see cref="IElementFactory.GetElement"/> method.
/// </summary>
public class ElementFactoryRecycleArgs
{
/// <summary>
/// Gets or sets the <see cref="IControl"/> to recycle when calling
/// <see cref="IElementFactory.RecycleElement"/>.
/// </summary>
public IControl Element { get; set; }
/// <summary>
/// Gets or sets the <see cref="IControl"/> that is expected to be the parent of the
/// realized element from <see cref="IElementFactory.GetElement"/>.
/// </summary>
public IControl Parent { get; set; }
}
/// <summary>
/// A data template that supports creating and recyling elements for an <see cref="ItemsRepeater"/>.
/// </summary>
public interface IElementFactory : IDataTemplate
{
/// <summary>
/// Gets an <see cref="IControl"/>.
/// </summary>
/// <param name="args">The element args.</param>
public IControl GetElement(ElementFactoryGetArgs args);
/// <summary>
/// Recycles an <see cref="IControl"/> that was previously retrieved using
/// <see cref="GetElement"/>.
/// </summary>
/// <param name="args">The recycle args.</param>
public void RecycleElement(ElementFactoryRecycleArgs args);
}
}

19
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);

4
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@ -141,7 +141,7 @@ namespace Avalonia.Controls
/// </summary>
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();
}

12
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<IDataTemplate> OriginTemplateProperty =
AvaloniaProperty.RegisterAttached<Control, IDataTemplate>("OriginTemplate", typeof(RecyclePool));
internal static readonly AttachedProperty<IDataTemplate> OriginTemplateProperty =
AvaloniaProperty.RegisterAttached<RecyclePool, Control, IDataTemplate>("OriginTemplate");
internal static readonly AttachedProperty<string> ReuseKeyProperty =
AvaloniaProperty.RegisterAttached<RecyclePool, Control, string>("ReuseKey", string.Empty);
private static ConditionalWeakTable<IDataTemplate, RecyclePool> s_pools = new ConditionalWeakTable<IDataTemplate, RecyclePool>();
private readonly Dictionary<string, List<ElementInfo>> _elements = new Dictionary<string, List<ElementInfo>>();
@ -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)

119
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<string, IDataTemplate>? _templates;
private SelectTemplateEventArgs? _args;
public RecyclingElementFactory()
{
Templates = new Dictionary<string, IDataTemplate>();
}
public RecyclePool RecyclePool
{
get => _recyclePool ??= new RecyclePool();
set => _recyclePool = value ?? throw new ArgumentNullException(nameof(value));
}
public IDictionary<string, IDataTemplate> Templates
{
get => _templates ??= new Dictionary<string, IDataTemplate>();
set => _templates = value ?? throw new ArgumentNullException(nameof(value));
}
public event EventHandler<SelectTemplateEventArgs>? 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!;
}
}
}

31
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;
}
}

29
src/Avalonia.Controls/Templates/FuncDataTemplate.cs

@ -6,7 +6,7 @@ namespace Avalonia.Controls.Templates
/// <summary>
/// Builds a control for a piece of data.
/// </summary>
public class FuncDataTemplate : FuncTemplate<object, IControl>, IDataTemplate
public class FuncDataTemplate : FuncTemplate<object, IControl>, IRecyclingDataTemplate
{
/// <summary>
/// The default data template used in the case where no matching data template is found.
@ -30,10 +30,8 @@ namespace Avalonia.Controls.Templates
},
true);
/// <summary>
/// The implementation of the <see cref="Match"/> method.
/// </summary>
private readonly Func<object, bool> _match;
private readonly bool _supportsRecycling;
/// <summary>
/// Initializes a new instance of the <see cref="FuncDataTemplate"/> class.
@ -70,12 +68,9 @@ namespace Avalonia.Controls.Templates
Contract.Requires<ArgumentNullException>(match != null);
_match = match;
SupportsRecycling = supportsRecycling;
_supportsRecycling = supportsRecycling;
}
/// <inheritdoc/>
public bool SupportsRecycling { get; }
/// <summary>
/// Checks to see if this data template matches the specified data.
/// </summary>
@ -88,6 +83,24 @@ namespace Avalonia.Controls.Templates
return _match(data);
}
/// <summary>
/// Creates or recycles a control to display the specified data.
/// </summary>
/// <param name="data">The data to display.</param>
/// <param name="existing">An optional control to recycle.</param>
/// <returns>
/// The <paramref name="existing"/> control if supplied and applicable to
/// <paramref name="data"/>, otherwise a new control.
/// </returns>
/// <remarks>
/// The caller should ensure that any control passed to <paramref name="existing"/>
/// originated from the same data template.
/// </remarks>
public IControl Build(object data, IControl existing)
{
return _supportsRecycling && existing is object ? existing : Build(data);
}
/// <summary>
/// Determines of an object is of the specified type.
/// </summary>

6
src/Avalonia.Controls/Templates/FuncTemplate`2.cs

@ -1,5 +1,7 @@
using System;
#nullable enable
namespace Avalonia.Controls.Templates
{
/// <summary>
@ -18,9 +20,7 @@ namespace Avalonia.Controls.Templates
/// <param name="func">The function used to create the control.</param>
public FuncTemplate(Func<TParam, INameScope, TControl> func)
{
Contract.Requires<ArgumentNullException>(func != null);
_func = func;
_func = func ?? throw new ArgumentNullException(nameof(func));
}
/// <summary>

12
src/Avalonia.Controls/Templates/IDataTemplate.cs

@ -1,3 +1,7 @@
using System;
#nullable enable
namespace Avalonia.Controls.Templates
{
/// <summary>
@ -5,12 +9,6 @@ namespace Avalonia.Controls.Templates
/// </summary>
public interface IDataTemplate : ITemplate<object, IControl>
{
/// <summary>
/// Gets a value indicating whether the data template supports recycling of the generated
/// control.
/// </summary>
bool SupportsRecycling { get; }
/// <summary>
/// Checks to see if this data template matches the specified data.
/// </summary>
@ -20,4 +18,4 @@ namespace Avalonia.Controls.Templates
/// </returns>
bool Match(object data);
}
}
}

25
src/Avalonia.Controls/Templates/IRecyclingDataTemplate.cs

@ -0,0 +1,25 @@
#nullable enable
namespace Avalonia.Controls.Templates
{
/// <summary>
/// An <see cref="IDataTemplate"/> that supports recycling existing elements.
/// </summary>
public interface IRecyclingDataTemplate : IDataTemplate
{
/// <summary>
/// Creates or recycles a control to display the specified data.
/// </summary>
/// <param name="data">The data to display.</param>
/// <param name="existing">An optional control to recycle.</param>
/// <returns>
/// The <paramref name="existing"/> control if supplied and applicable to
/// <paramref name="data"/>, otherwise a new control.
/// </returns>
/// <remarks>
/// The caller should ensure that any control passed to <paramref name="existing"/>
/// originated from the same data template.
/// </remarks>
IControl Build(object data, IControl? existing);
}
}

2
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;
}

25
src/Avalonia.Controls/TickBar.cs

@ -39,11 +39,15 @@ namespace Avalonia.Controls
{
static TickBar()
{
AffectsRender<TickBar>(ReservedSpaceProperty,
AffectsRender<TickBar>(FillProperty,
IsDirectionReversedProperty,
ReservedSpaceProperty,
MaximumProperty,
MinimumProperty,
OrientationProperty,
TickFrequencyProperty);
PlacementProperty,
TickFrequencyProperty,
TicksProperty);
}
public TickBar() : base()
@ -137,7 +141,7 @@ namespace Avalonia.Controls
/// <summary>
/// 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 <see cref="DoubleCollection" />.
/// The property value is a <see cref="AvaloniaList{T}" />.
/// </summary>
public AvaloniaList<double> Ticks
{
@ -169,7 +173,6 @@ namespace Avalonia.Controls
public static readonly StyledProperty<TickBarPlacement> PlacementProperty =
AvaloniaProperty.Register<TickBar, TickBarPlacement>(nameof(Placement), 0d);
/// <summary>
/// Placement property specified how the Tick will be placed.
/// This property affects the way ticks are drawn.
@ -189,7 +192,7 @@ namespace Avalonia.Controls
/// <summary>
/// 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 <see cref="Rect" />.
/// </summary>
@ -201,7 +204,7 @@ namespace Avalonia.Controls
/// <summary>
/// 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.
/// </summary>
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

8
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);
}
/// <summary>
/// Called when the pointer enters a control with an attached tooltip.

4
src/Avalonia.Controls/TopLevel.cs

@ -280,10 +280,10 @@ namespace Avalonia.Controls
}
/// <inheritdoc/>
double ILayoutRoot.LayoutScaling => PlatformImpl?.Scaling ?? 1;
double ILayoutRoot.LayoutScaling => PlatformImpl?.RenderScaling ?? 1;
/// <inheritdoc/>
double IRenderRoot.RenderScaling => PlatformImpl?.Scaling ?? 1;
double IRenderRoot.RenderScaling => PlatformImpl?.RenderScaling ?? 1;
IStyleHost IStyleHost.StylingParent => _globalStyles;

20
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
/// </summary>
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;
}
}
/// <summary>
@ -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(

7
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<AvaloniaXamlLoader.IRuntimeXamlLoader>();
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)
{

10
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);

28
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",

1
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<PixelPoint> PositionChanged { get; set; }
public Action Deactivated { get; set; }

7
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));

3
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<object> Surfaces { get; }
public Action<RawInputEventArgs> Input { get; set; }
public Action<Rect> Paint { get; set; }

2
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");

2
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; }

9
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<object> Surfaces { get; }
public Action<RawInputEventArgs> Input { get; set; }
public Action<Rect> 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, () =>
{

15
src/Avalonia.Native/OsxManagedPopupPositionerPopupImplHelper.cs

@ -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;
}
}

5
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();
}
}

2
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)

8
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);
}
}

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

@ -40,6 +40,8 @@
<TextBlock Name="watermark"
Opacity="0.5"
Text="{TemplateBinding Watermark}"
TextAlignment="{TemplateBinding TextAlignment}"
TextWrapping="{TemplateBinding TextWrapping}"
IsVisible="{TemplateBinding Text, Converter={x:Static StringConverters.IsNullOrEmpty}}"/>
<TextPresenter Name="PART_TextPresenter"
Text="{TemplateBinding Text, Mode=TwoWay}"

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

@ -15,7 +15,7 @@
<Panel x:Name="PART_MouseTracker" Height="1" VerticalAlignment="Top" />
<Panel x:Name="PART_Container">
<Border x:Name="PART_Background" Background="{TemplateBinding Background}" />
<CaptionButtons x:Name="PART_CaptionButtons" VerticalAlignment="Top" HorizontalAlignment="Right" Foreground="{TemplateBinding Foreground}" MaxHeight="30" />
<CaptionButtons x:Name="PART_CaptionButtons" VerticalAlignment="Top" HorizontalAlignment="Right" Foreground="{TemplateBinding Foreground}" />
</Panel>
</Panel>
</ControlTemplate>

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

@ -43,7 +43,8 @@
<Setter Property="Foreground" Value="{DynamicResource ToggleSwitchContentForeground}" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="Template">
<ControlTemplate>

34
src/Avalonia.Themes.Default/Window.xaml

@ -1,23 +1,25 @@
<Style xmlns="https://github.com/avaloniaui" Selector="Window">
<Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/>
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}"/>
<Setter Property="FontSize" Value="{DynamicResource FontSizeNormal}"/>
<Setter Property="Background" Value="{DynamicResource SystemControlBackgroundAltHighBrush}"/>
<Setter Property="TransparencyBackgroundFallback" Value="{DynamicResource SystemControlBackgroundAltHighBrush}" />
<Setter Property="Foreground" Value="{DynamicResource SystemControlForegroundBaseHighBrush}"/>
<Setter Property="FontSize" Value="{DynamicResource ContentControlFontSize}"/>
<Setter Property="FontFamily" Value="{DynamicResource ContentControlThemeFontFamily}" />
<Setter Property="Template">
<ControlTemplate>
<Panel>
<Panel IsHitTestVisible="False" Margin="{TemplateBinding OffScreenMargin}">
<Border Name="PART_TransparencyFallback" IsHitTestVisible="False" />
</Panel>
<Border Background="{TemplateBinding Background}">
<VisualLayerManager>
<ContentPresenter Name="PART_ContentPresenter"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Margin="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>
</VisualLayerManager>
</Border>
<Border Name="PART_TransparencyFallback" IsHitTestVisible="False" />
<Border Background="{TemplateBinding Background}" IsHitTestVisible="False" />
<VisualLayerManager>
<VisualLayerManager.ChromeOverlayLayer>
<TitleBar />
</VisualLayerManager.ChromeOverlayLayer>
<ContentPresenter Name="PART_ContentPresenter"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Margin="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>
</VisualLayerManager>
</Panel>
</ControlTemplate>
</Setter>

6
src/Avalonia.Themes.Fluent/Menu.xaml

@ -11,10 +11,12 @@
</Design.PreviewWith>
<Style.Resources>
<x:Double x:Key="MenuHeight">32</x:Double>
<x:Double x:Key="MenuBarHeight">32</x:Double>
<Thickness x:Key="MenuBarItemPadding">12,0,12,0</Thickness>
</Style.Resources>
<Setter Property="Background" Value="Transparent" />
<Setter Property="Height" Value="{StaticResource MenuHeight}" />
<Setter Property="Height" Value="{StaticResource MenuBarHeight}" />
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}"

184
src/Avalonia.Themes.Fluent/MenuItem.xaml

@ -25,11 +25,12 @@
</MenuItem>
<MenuItem Header="Edit">
<MenuItem Header="Go To">
<MenuItem Header="Go To Line"/>
<MenuItem Header="Go To Line" />
</MenuItem>
</MenuItem>
<MenuItem Header="View">
<MenuItem Header="Designer" InputGesture="Shift+F7" />
<MenuItem Header="Designer"
InputGesture="Shift+F7" />
</MenuItem>
<MenuItem Header="Project">
<MenuItem Header="Add class" />
@ -40,9 +41,10 @@
<Styles.Resources>
<conv:PlatformKeyGestureConverter x:Key="KeyGestureConverter" />
<x:Double x:Key="MenuFlyoutSubItemPopupHorizontalOffset">-4</x:Double>
<Thickness x:Key="MenuFlyoutScrollerMargin">0,4,0,4</Thickness>
<Thickness x:Key="MenuIconPresenterMargin">0,0,12,0</Thickness>
<Thickness x:Key="MenuInputGestureTextMargin">24,0,0,0</Thickness>
<Thickness x:Key="MenuInputGestureTextMargin">24,0,0,0</Thickness>
<StreamGeometry x:Key="MenuItemChevronPathData">M 1,0 10,10 l -9,10 -1,-1 L 8,10 -0,1 Z</StreamGeometry>
</Styles.Resources>
@ -54,83 +56,84 @@
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="Template">
<ControlTemplate>
<Border Name="PART_LayoutRoot"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"
SharedSizeGroup="MenuItemIcon" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto"
SharedSizeGroup="MenuItemIGT" />
<ColumnDefinition Width="Auto"
SharedSizeGroup="MenuItemChevron" />
</Grid.ColumnDefinitions>
<ContentPresenter Name="PART_IconPresenter"
Content="{TemplateBinding Icon}"
Width="16"
Height="16"
Margin="{DynamicResource MenuIconPresenterMargin}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
<Panel>
<Border Name="PART_LayoutRoot"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"
SharedSizeGroup="MenuItemIcon" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto"
SharedSizeGroup="MenuItemIGT" />
<ColumnDefinition Width="Auto"
SharedSizeGroup="MenuItemChevron" />
</Grid.ColumnDefinitions>
<ContentPresenter Name="PART_IconPresenter"
Content="{TemplateBinding Icon}"
Width="16"
Height="16"
Margin="{DynamicResource MenuIconPresenterMargin}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
<ContentPresenter Name="PART_HeaderPresenter"
Content="{TemplateBinding Header}"
VerticalAlignment="Center"
HorizontalAlignment="Stretch"
TextBlock.Foreground="{TemplateBinding Foreground}"
Grid.Column="1">
<ContentPresenter.DataTemplates>
<DataTemplate DataType="sys:String">
<AccessText Text="{Binding}" />
</DataTemplate>
</ContentPresenter.DataTemplates>
</ContentPresenter>
<TextBlock x:Name="PART_InputGestureText"
Grid.Column="2"
Classes="CaptionTextBlockStyle"
Margin="{DynamicResource MenuInputGestureTextMargin}"
Text="{TemplateBinding InputGesture,
Converter={StaticResource KeyGestureConverter}}"
HorizontalAlignment="Right"
VerticalAlignment="Center" />
<Path Name="PART_ChevronPath"
Stretch="Uniform"
Width="8"
Height="16"
Data="{StaticResource MenuItemChevronPathData}"
Margin="{DynamicResource MenuFlyoutItemChevronMargin}"
VerticalAlignment="Center"
Grid.Column="3" />
<Popup Name="PART_Popup"
WindowManagerAddShadowHint="True"
PlacementMode="Right"
StaysOpen="True"
IsOpen="{TemplateBinding IsSubMenuOpen,
Mode=TwoWay}">
<Border Background="{DynamicResource MenuFlyoutPresenterBackground}"
BorderBrush="{DynamicResource MenuFlyoutPresenterBorderBrush}"
BorderThickness="{DynamicResource MenuFlyoutPresenterBorderThemeThickness}"
Padding="{DynamicResource MenuFlyoutPresenterThemePadding}"
MaxWidth="{DynamicResource FlyoutThemeMaxWidth}"
MinHeight="{DynamicResource MenuFlyoutThemeMinHeight}"
HorizontalAlignment="Stretch"
CornerRadius="{DynamicResource OverlayCornerRadius}">
<ScrollViewer>
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
Margin="{DynamicResource MenuFlyoutScrollerMargin}"
Grid.IsSharedSizeScope="True" />
</ScrollViewer>
</Border>
</Popup>
</Grid>
</Border>
<ContentPresenter Name="PART_HeaderPresenter"
Content="{TemplateBinding Header}"
VerticalAlignment="Center"
HorizontalAlignment="Stretch"
TextBlock.Foreground="{TemplateBinding Foreground}"
Grid.Column="1">
<ContentPresenter.DataTemplates>
<DataTemplate DataType="sys:String">
<AccessText Text="{Binding}" />
</DataTemplate>
</ContentPresenter.DataTemplates>
</ContentPresenter>
<TextBlock x:Name="PART_InputGestureText"
Grid.Column="2"
Classes="CaptionTextBlockStyle"
Margin="{DynamicResource MenuInputGestureTextMargin}"
Text="{TemplateBinding InputGesture, Converter={StaticResource KeyGestureConverter}}"
HorizontalAlignment="Right"
VerticalAlignment="Center" />
<Path Name="PART_ChevronPath"
Stretch="Uniform"
Width="8"
Height="16"
Data="{StaticResource MenuItemChevronPathData}"
Margin="{DynamicResource MenuFlyoutItemChevronMargin}"
VerticalAlignment="Center"
Grid.Column="3" />
</Grid>
</Border>
<Popup Name="PART_Popup"
WindowManagerAddShadowHint="True"
PlacementMode="Right"
HorizontalOffset="{StaticResource MenuFlyoutSubItemPopupHorizontalOffset}"
StaysOpen="True"
IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}">
<Border Background="{DynamicResource MenuFlyoutPresenterBackground}"
BorderBrush="{DynamicResource MenuFlyoutPresenterBorderBrush}"
BorderThickness="{DynamicResource MenuFlyoutPresenterBorderThemeThickness}"
Padding="{DynamicResource MenuFlyoutPresenterThemePadding}"
MaxWidth="{DynamicResource FlyoutThemeMaxWidth}"
MinHeight="{DynamicResource MenuFlyoutThemeMinHeight}"
HorizontalAlignment="Stretch"
CornerRadius="{DynamicResource OverlayCornerRadius}">
<ScrollViewer>
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
Margin="{DynamicResource MenuFlyoutScrollerMargin}"
Grid.IsSharedSizeScope="True" />
</ScrollViewer>
</Border>
</Popup>
</Panel>
</ControlTemplate>
</Setter>
</Style>
@ -145,6 +148,8 @@
<Panel>
<ContentPresenter Name="PART_HeaderPresenter"
Content="{TemplateBinding Header}"
VerticalAlignment="Center"
HorizontalAlignment="Stretch"
Margin="{TemplateBinding Padding}">
<ContentPresenter.DataTemplates>
<DataTemplate DataType="sys:String">
@ -155,8 +160,7 @@
<Popup Name="PART_Popup"
WindowManagerAddShadowHint="False"
MinWidth="{Binding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}"
IsOpen="{TemplateBinding IsSubMenuOpen,
Mode=TwoWay}"
IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}"
StaysOpen="True">
<Border Background="{DynamicResource MenuFlyoutPresenterBackground}"
BorderBrush="{DynamicResource MenuFlyoutPresenterBorderBrush}"
@ -183,10 +187,15 @@
</Style>
<Style Selector="MenuItem">
<!-- Narrow padding should be used for mouse input, when non-narrow one should be used for touch input in future. -->
<Setter Property="Padding" Value="{DynamicResource MenuFlyoutItemThemePaddingNarrow}" />
<!-- Narrow padding should be used for mouse input, when non-narrow one should be used for touch input in future. -->
<Setter Property="Padding" Value="{StaticResource MenuFlyoutItemThemePaddingNarrow}" />
</Style>
<Style Selector="Menu > MenuItem">
<!-- Custom padding for Menu > MenuItem -->
<Setter Property="Padding" Value="{StaticResource MenuBarItemPadding}" />
</Style>
<Style Selector="MenuItem /template/ ContentPresenter#PART_IconPresenter">
<Setter Property="IsVisible" Value="False" />
</Style>
@ -212,14 +221,15 @@
<Setter Property="Fill" Value="{DynamicResource MenuFlyoutSubItemChevronPointerOver}" />
</Style>
<Style Selector="MenuItem:pressed /template/ Border#PART_LayoutRoot">
<!-- Listen for PART_LayoutRoot:pointerover, so it will not be triggered when subitem is pressed -->
<Style Selector="MenuItem:pressed /template/ Border#PART_LayoutRoot:pointerover">
<Setter Property="Background" Value="{DynamicResource MenuFlyoutItemBackgroundPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource MenuFlyoutItemBorderBrushPressed}" />
</Style>
<Style Selector="MenuItem:pressed /template/ TextBlock#PART_InputGestureText">
<Style Selector="MenuItem:pressed /template/ Border#PART_LayoutRoot:pointerover TextBlock#PART_InputGestureText">
<Setter Property="Foreground" Value="{DynamicResource MenuFlyoutItemKeyboardAcceleratorTextForegroundPressed}" />
</Style>
<Style Selector="MenuItem:pressed /template/ Path#PART_ChevronPath">
<Style Selector="MenuItem:pressed /template/ Border#PART_LayoutRoot:pointerover Path#PART_ChevronPath">
<Setter Property="Fill" Value="{DynamicResource MenuFlyoutSubItemChevronPressed}" />
</Style>

2
src/Avalonia.Themes.Fluent/NumericUpDown.xaml

@ -43,7 +43,7 @@
<TextBox Name="PART_TextBox"
Background="Transparent"
BorderBrush="Transparent"
Margin="-2"
Margin="-1"
Padding="{TemplateBinding Padding}"
Watermark="{TemplateBinding Watermark}"
DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}"

107
src/Avalonia.Themes.Fluent/TextBox.xaml

@ -10,71 +10,65 @@
<Setter Property="SelectionBrush" Value="{DynamicResource TextControlSelectionHighlightColor}" />
<Setter Property="BorderThickness" Value="{DynamicResource TextControlBorderThemeThickness}" />
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Hidden" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Hidden" />
<Setter Property="MinHeight" Value="{DynamicResource TextControlThemeMinHeight}" />
<Setter Property="MinWidth" Value="{DynamicResource TextControlThemeMinWidth}" />
<Setter Property="Padding" Value="{DynamicResource TextControlThemePadding}" />
<Setter Property="Template">
<ControlTemplate>
<Grid RowDefinitions="Auto, *, Auto" ColumnDefinitions="*, Auto">
<DockPanel>
<!-- TODO bind Content -> Header and ContentTemplate -> HeaderTemplate -->
<ContentPresenter x:Name="HeaderContentPresenter"
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="2"
DockPanel.Dock="Top"
TextBlock.FontWeight="Normal"
TextBlock.Foreground="{DynamicResource TextControlHeaderForeground}"
IsVisible="False"
Margin="{DynamicResource TextBoxTopHeaderMargin}" />
<Border Name="border"
Grid.Row="1"
Grid.Column="0"
Grid.RowSpan="1"
Grid.ColumnSpan="2"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{DynamicResource ControlCornerRadius}"
Margin="{TemplateBinding BorderThickness}"
MinWidth="{TemplateBinding MinWidth}"
MinHeight="{TemplateBinding MinHeight}">
</Border>
<Border Padding="{TemplateBinding Padding}"
Grid.Row="1"
Grid.Column="0"
Margin="{TemplateBinding BorderThickness}">
<DockPanel>
<TextBlock Name="floatingWatermark"
Foreground="{DynamicResource SystemAccentColor}"
FontSize="{DynamicResource FontSizeSmall}"
Text="{TemplateBinding Watermark}"
DockPanel.Dock="Top">
<TextBlock.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="UseFloatingWatermark"/>
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="Text"
Converter="{x:Static StringConverters.IsNotNullOrEmpty}"/>
</MultiBinding>
</TextBlock.IsVisible>
</TextBlock>
<DataValidationErrors>
<ScrollViewer HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}">
<Panel>
<TextBlock Name="watermark"
<Panel>
<Border
Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{DynamicResource ControlCornerRadius}"
MinWidth="{TemplateBinding MinWidth}"
MinHeight="{TemplateBinding MinHeight}">
</Border>
<Border
Padding="{TemplateBinding Padding}"
Margin="{TemplateBinding BorderThickness}">
<DockPanel>
<TextBlock Name="floatingWatermark"
Foreground="{DynamicResource SystemAccentColor}"
FontSize="{DynamicResource FontSizeSmall}"
Text="{TemplateBinding Watermark}"
DockPanel.Dock="Top">
<TextBlock.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="UseFloatingWatermark"/>
<Binding RelativeSource="{RelativeSource TemplatedParent}"
Path="Text"
Converter="{x:Static StringConverters.IsNotNullOrEmpty}"/>
</MultiBinding>
</TextBlock.IsVisible>
</TextBlock>
<DataValidationErrors>
<ScrollViewer HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
VerticalScrollBarVisibility="{TemplateBinding (ScrollViewer.VerticalScrollBarVisibility)}">
<Panel>
<TextBlock Name="watermark"
Opacity="0.5"
Text="{TemplateBinding Watermark}"
TextAlignment="{TemplateBinding TextAlignment}"
TextWrapping="{TemplateBinding TextWrapping}"
IsVisible="{TemplateBinding Text, Converter={x:Static StringConverters.IsNullOrEmpty}}"/>
<!-- TODO eliminate this margin... text layout issue? -->
<TextPresenter Name="PART_TextPresenter"
<!-- TODO eliminate this margin... text layout issue? -->
<TextPresenter Name="PART_TextPresenter"
Margin="0 1 0 0"
Text="{TemplateBinding Text, Mode=TwoWay}"
CaretIndex="{TemplateBinding CaretIndex}"
@ -86,12 +80,13 @@
SelectionBrush="{TemplateBinding SelectionBrush}"
SelectionForegroundBrush="{TemplateBinding SelectionForegroundBrush}"
CaretBrush="{TemplateBinding CaretBrush}"/>
</Panel>
</ScrollViewer>
</DataValidationErrors>
</DockPanel>
</Border>
</Grid>
</Panel>
</ScrollViewer>
</DataValidationErrors>
</DockPanel>
</Border>
</Panel>
</DockPanel>
</ControlTemplate>
</Setter>
</Style>

2
src/Avalonia.Themes.Fluent/TitleBar.xaml

@ -15,7 +15,7 @@
<Panel x:Name="PART_MouseTracker" Height="1" VerticalAlignment="Top" />
<Panel x:Name="PART_Container">
<Border x:Name="PART_Background" Background="{TemplateBinding Background}" />
<CaptionButtons x:Name="PART_CaptionButtons" VerticalAlignment="Top" HorizontalAlignment="Right" Foreground="{TemplateBinding Foreground}" MaxHeight="30" />
<CaptionButtons x:Name="PART_CaptionButtons" VerticalAlignment="Top" HorizontalAlignment="Right" Foreground="{TemplateBinding Foreground}" />
</Panel>
</Panel>
</ControlTemplate>

3
src/Avalonia.Themes.Fluent/ToggleSwitch.xaml

@ -43,7 +43,8 @@
<Setter Property="Foreground" Value="{DynamicResource ToggleSwitchContentForeground}" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="Template">
<ControlTemplate>

22
src/Avalonia.Themes.Fluent/Window.xaml

@ -8,16 +8,18 @@
<ControlTemplate>
<Panel>
<Border Name="PART_TransparencyFallback" IsHitTestVisible="False" />
<Border Background="{TemplateBinding Background}">
<VisualLayerManager>
<ContentPresenter Name="PART_ContentPresenter"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Margin="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>
</VisualLayerManager>
</Border>
<Border Background="{TemplateBinding Background}" IsHitTestVisible="False" />
<VisualLayerManager>
<VisualLayerManager.ChromeOverlayLayer>
<TitleBar />
</VisualLayerManager.ChromeOverlayLayer>
<ContentPresenter Name="PART_ContentPresenter"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Margin="{TemplateBinding Padding}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>
</VisualLayerManager>
</Panel>
</ControlTemplate>
</Setter>

15
src/Avalonia.Visuals/Media/Color.cs

@ -89,6 +89,11 @@ namespace Avalonia.Media
/// <returns>The <see cref="Color"/>.</returns>
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
/// <returns>The status of the operation.</returns>
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;
}

15
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;
/// <summary>
@ -50,13 +44,6 @@
{
_textWrapping = textWrapping;
}
/// <summary>
/// Set text trimming
/// </summary>
internal void SetTextTrimming(TextTrimming textTrimming)
{
_textTrimming = textTrimming;
}
/// <summary>
/// Set line height

3
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
{

23
src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs

@ -0,0 +1,23 @@
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// Properties of text collapsing
/// </summary>
public abstract class TextCollapsingProperties
{
/// <summary>
/// Gets the width in which the collapsible range is constrained to
/// </summary>
public abstract double Width { get; }
/// <summary>
/// Gets the text run that is used as collapsing symbol
/// </summary>
public abstract TextRun Symbol { get; }
/// <summary>
/// Gets the style of collapsing
/// </summary>
public abstract TextCollapsingStyle Style { get; }
}
}

18
src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingStyle.cs

@ -0,0 +1,18 @@
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// Text collapsing style
/// </summary>
public enum TextCollapsingStyle
{
/// <summary>
/// Collapse trailing characters
/// </summary>
TrailingCharacter,
/// <summary>
/// Collapse trailing words
/// </summary>
TrailingWord,
}
}

503
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<char> s_ellipsis = new ReadOnlySlice<char>(new[] { '\u2026' });
/// <inheritdoc cref="TextFormatter.FormatLine"/>
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;
}
/// <summary>
/// Measures the number of characters that fits into available width.
/// </summary>
/// <param name="textCharacters">The text run.</param>
/// <param name="availableWidth">The available width.</param>
/// <returns></returns>
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;
}
/// <summary>
/// Split a sequence of runs into two segments at specified length.
/// </summary>
/// <param name="textRuns">The text run's.</param>
/// <param name="length">The length to split at.</param>
/// <returns>The split text runs.</returns>
internal static SplitTextRunsResult SplitTextRuns(IReadOnlyList<ShapedTextCharacters> 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);
}
/// <summary>
@ -174,87 +316,6 @@ namespace Avalonia.Media.TextFormatting
return false;
}
/// <summary>
/// Performs text trimming and returns a trimmed line.
/// </summary>
/// <param name="textRuns">The text runs to perform the trimming on.</param>
/// <param name="textRange">The text range that is covered by the text runs.</param>
/// <param name="paragraphWidth">A <see cref="double"/> value that specifies the width of the paragraph that the line fills.</param>
/// <param name="paragraphProperties">A <see cref="TextParagraphProperties"/> value that represents paragraph properties,
/// such as TextWrapping, TextAlignment, or TextStyle.</param>
/// <returns></returns>
private static TextLine PerformTextTrimming(IReadOnlyList<ShapedTextCharacters> 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<ShapedTextCharacters>(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));
}
/// <summary>
/// Performs text wrapping returns a list of text lines.
/// </summary>
@ -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));
}
/// <summary>
/// Measures the number of characters that fits into available width.
/// </summary>
/// <param name="textCharacters">The text run.</param>
/// <param name="availableWidth">The available width.</param>
/// <returns></returns>
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;
}
/// <summary>
/// Creates an ellipsis.
/// </summary>
/// <param name="properties">The text run properties.</param>
/// <returns></returns>
private static ShapedTextCharacters CreateEllipsisRun(TextRunProperties properties)
{
var formatterImpl = AvaloniaLocator.Current.GetService<ITextShaperImpl>();
var glyphRun = formatterImpl.ShapeText(s_ellipsis, properties.Typeface, properties.FontRenderingEmSize,
properties.CultureInfo);
return new ShapedTextCharacters(glyphRun, properties);
}
/// <summary>
/// Gets the text range that is covered by the text runs.
/// </summary>
@ -464,86 +432,7 @@ namespace Avalonia.Media.TextFormatting
return new TextRange(start, end - start);
}
/// <summary>
/// Split a sequence of runs into two segments at specified length.
/// </summary>
/// <param name="textRuns">The text run's.</param>
/// <param name="length">The length to split at.</param>
/// <returns>The split text runs.</returns>
private static SplitTextRunsResult SplitTextRuns(IReadOnlyList<ShapedTextCharacters> 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<ShapedTextCharacters> first, IReadOnlyList<ShapedTextCharacters> second)
{

67
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<char> _text;
private readonly TextParagraphProperties _paragraphProperties;
private readonly IReadOnlyList<ValueSpan<TextRunProperties>> _textStyleOverrides;
private readonly TextTrimming _textTrimming;
/// <summary>
/// Initializes a new instance of the <see cref="TextLayout" /> class.
@ -54,9 +53,11 @@ namespace Avalonia.Media.TextFormatting
new ReadOnlySlice<char>(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
/// <param name="foreground">The foreground.</param>
/// <param name="textAlignment">The text alignment.</param>
/// <param name="textWrapping">The text wrapping.</param>
/// <param name="textTrimming">The text trimming.</param>
/// <param name="textDecorations">The text decorations.</param>
/// <param name="lineHeight">The height of each line of text.</param>
/// <returns></returns>
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);
}
/// <summary>
@ -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
}
}
/// <summary>
/// Gets the <see cref="TextCollapsingProperties"/> for current text trimming mode.
/// </summary>
/// <param name="width">The collapsing width.</param>
/// <returns>The <see cref="TextCollapsingProperties"/>.</returns>
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<char> _text;

45
src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs

@ -39,6 +39,14 @@ namespace Avalonia.Media.TextFormatting
/// </returns>
public abstract TextLineBreak LineBreak { get; }
/// <summary>
/// Gets a value that indicates whether the line is collapsed.
/// </summary>
/// <returns>
/// <c>true</c>, if the line is collapsed; otherwise, <c>false</c>.
/// </returns>
public abstract bool HasCollapsed { get; }
/// <summary>
/// Draws the <see cref="TextLine"/> at the given origin.
/// </summary>
@ -47,40 +55,49 @@ namespace Avalonia.Media.TextFormatting
public abstract void Draw(DrawingContext drawingContext, Point origin);
/// <summary>
/// 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.
/// </summary>
/// <param name="collapsingPropertiesList">A list of <see cref="TextCollapsingProperties"/>
/// objects that represent the collapsed text properties.</param>
/// <returns>
/// A <see cref="TextLine"/> value that represents a collapsed line that can be displayed.
/// </returns>
public abstract TextLine Collapse(params TextCollapsingProperties[] collapsingPropertiesList);
/// <summary>
/// Gets the character hit corresponding to the specified distance from the beginning of the line.
/// </summary>
/// <param name="distance">distance in text flow direction from the beginning of the line</param>
/// <returns>The <see cref="CharacterHit"/></returns>
/// <param name="distance">A <see cref="double"/> value that represents the distance from the beginning of the line.</param>
/// <returns>The <see cref="CharacterHit"/> object at the specified distance from the beginning of the line.</returns>
public abstract CharacterHit GetCharacterHitFromDistance(double distance);
/// <summary>
/// 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.
/// <see cref="CharacterHit"/>.
/// </summary>
/// <param name="characterHit"><see cref="CharacterHit"/> of the character to query the distance.</param>
/// <returns>Distance in text flow direction from the beginning of the line.</returns>
/// <param name="characterHit">The <see cref="CharacterHit"/> object whose distance you want to query.</param>
/// <returns>A <see cref="double"/> that represents the distance from the beginning of the line.</returns>
public abstract double GetDistanceFromCharacterHit(CharacterHit characterHit);
/// <summary>
/// Client to get the next <see cref="CharacterHit"/> for caret navigation.
/// Gets the next character hit for caret navigation.
/// </summary>
/// <param name="characterHit">The current <see cref="CharacterHit"/>.</param>
/// <returns>The next <see cref="CharacterHit"/>.</returns>
public abstract CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit);
/// <summary>
/// Client to get the previous character hit for caret navigation
/// Gets the previous character hit for caret navigation.
/// </summary>
/// <param name="characterHit">the current character hit</param>
/// <returns>The previous <see cref="CharacterHit"/></returns>
/// <param name="characterHit">The current <see cref="CharacterHit"/>.</param>
/// <returns>The previous <see cref="CharacterHit"/>.</returns>
public abstract CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit);
/// <summary>
/// Client to get the previous character hit after backspacing
/// Gets the previous character hit after backspacing.
/// </summary>
/// <param name="characterHit">the current character hit</param>
/// <returns>The <see cref="CharacterHit"/> after backspacing</returns>
/// <param name="characterHit">The current <see cref="CharacterHit"/>.</param>
/// <returns>The <see cref="CharacterHit"/> after backspacing.</returns>
public abstract CharacterHit GetBackspaceCaretCharacterHit(CharacterHit characterHit);
/// <summary>

157
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<ShapedTextCharacters> _textRuns;
public TextLineImpl(IReadOnlyList<ShapedTextCharacters> textRuns, TextLineMetrics lineMetrics,
TextLineBreak lineBreak = null)
TextLineBreak lineBreak = null, bool hasCollapsed = false)
{
_textRuns = textRuns;
LineMetrics = lineMetrics;
LineBreak = lineBreak;
HasCollapsed = hasCollapsed;
}
/// <inheritdoc/>
@ -26,6 +29,9 @@ namespace Avalonia.Media.TextFormatting
/// <inheritdoc/>
public override TextLineBreak LineBreak { get; }
/// <inheritdoc/>
public override bool HasCollapsed { get; }
/// <inheritdoc/>
public override void Draw(DrawingContext drawingContext, Point origin)
{
@ -41,6 +47,99 @@ namespace Avalonia.Media.TextFormatting
}
}
/// <inheritdoc/>
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<ShapedTextCharacters>(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<ShapedTextCharacters>(_textRuns) { shapedSymbol }, textLineMetrics, null,
true);
}
/// <inheritdoc/>
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
}
/// <inheritdoc/>
@ -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
}
/// <inheritdoc/>
@ -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;
}
/// <summary>
/// Creates a shaped symbol.
/// </summary>
/// <param name="textRun">The symbol run to shape.</param>
/// <returns>
/// The shaped symbol.
/// </returns>
internal static ShapedTextCharacters CreateShapedSymbol(TextRun textRun)
{
var formatterImpl = AvaloniaLocator.Current.GetService<ITextShaperImpl>();
var glyphRun = formatterImpl.ShapeText(textRun.Text, textRun.Properties.Typeface, textRun.Properties.FontRenderingEmSize,
textRun.Properties.CultureInfo);
return new ShapedTextCharacters(glyphRun, textRun.Properties);
}
/// <summary>
/// Gets the shaped width of specified shaped text characters.
/// </summary>
/// <param name="shapedTextCharacters">The shaped text characters.</param>
/// <returns>
/// The shaped width.
/// </returns>
private static double GetShapedWidth(IReadOnlyList<ShapedTextCharacters> shapedTextCharacters)
{
var shapedWidth = 0.0;
for (var i = 0; i < shapedTextCharacters.Count; i++)
{
shapedWidth += shapedTextCharacters[i].Bounds.Width;
}
return shapedWidth;
}
}
}

11
src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs

@ -9,11 +9,12 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
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;
}
/// <summary>
@ -37,6 +38,12 @@ namespace Avalonia.Media.TextFormatting
/// </summary>
public double TextBaseline { get; }
/// <summary>
/// Gets a boolean value that indicates whether content of the line overflows
/// the specified paragraph width.
/// </summary>
public bool HasOverflowed { get; }
/// <summary>
/// Creates the text line metrics.
/// </summary>
@ -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);
}
}
}

5
src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs

@ -26,11 +26,6 @@
/// </summary>
public abstract TextWrapping TextWrapping { get; }
/// <summary>
/// Gets the text trimming.
/// </summary>
public abstract TextTrimming TextTrimming { get; }
/// <summary>
/// Paragraph's line height
/// </summary>

7
src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs

@ -4,12 +4,11 @@ using System.Globalization;
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// 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.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public abstract class TextRunProperties : IEquatable<TextRunProperties>
{

33
src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs

@ -0,0 +1,33 @@
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// a collapsing properties to collapse whole line toward the end
/// at character granularity and with ellipsis being the collapsing symbol
/// </summary>
public class TextTrailingCharacterEllipsis : TextCollapsingProperties
{
private static readonly ReadOnlySlice<char> s_ellipsis = new ReadOnlySlice<char>(new[] { '\u2026' });
/// <summary>
/// Construct a text trailing character ellipsis collapsing properties
/// </summary>
/// <param name="width">width in which collapsing is constrained to</param>
/// <param name="textRunProperties">text run properties of ellispis symbol</param>
public TextTrailingCharacterEllipsis(double width, TextRunProperties textRunProperties)
{
Width = width;
Symbol = new TextCharacters(s_ellipsis, textRunProperties);
}
/// <inheritdoc/>
public sealed override double Width { get; }
/// <inheritdoc/>
public sealed override TextRun Symbol { get; }
/// <inheritdoc/>
public sealed override TextCollapsingStyle Style { get; } = TextCollapsingStyle.TrailingCharacter;
}
}

37
src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs

@ -0,0 +1,37 @@
using Avalonia.Utilities;
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// a collapsing properties to collapse whole line toward the end
/// at word granularity and with ellipsis being the collapsing symbol
/// </summary>
public class TextTrailingWordEllipsis : TextCollapsingProperties
{
private static readonly ReadOnlySlice<char> s_ellipsis = new ReadOnlySlice<char>(new[] { '\u2026' });
/// <summary>
/// Construct a text trailing word ellipsis collapsing properties
/// </summary>
/// <param name="width">width in which collapsing is constrained to</param>
/// <param name="textRunProperties">text run properties of ellispis symbol</param>
public TextTrailingWordEllipsis(
double width,
TextRunProperties textRunProperties
)
{
Width = width;
Symbol = new TextCharacters(s_ellipsis, textRunProperties);
}
/// <inheritdoc/>
public sealed override double Width { get; }
/// <inheritdoc/>
public sealed override TextRun Symbol { get; }
/// <inheritdoc/>
public sealed override TextCollapsingStyle Style { get; } = TextCollapsingStyle.TrailingWord;
}
}

2
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;
}

1
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

16
src/Avalonia.Visuals/Media/TextWrapping.cs

@ -5,13 +5,6 @@ namespace Avalonia.Media
/// </summary>
public enum TextWrapping
{
/// <summary>
/// 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.
/// </summary>
WrapWithOverflow,
/// <summary>
/// Text should not wrap.
/// </summary>
@ -20,6 +13,13 @@ namespace Avalonia.Media
/// <summary>
/// Text can wrap.
/// </summary>
Wrap
Wrap,
/// <summary>
/// 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.
/// </summary>
WrapWithOverflow
}
}

3
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);

45
src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs

@ -16,7 +16,7 @@ namespace Avalonia.X11.NativeDialogs
{
private Task<bool> _initialized;
private unsafe Task<string[]> ShowDialog(string title, IWindowImpl parent, GtkFileChooserAction action,
bool multiSelect, string initialFileName, IEnumerable<FileDialogFilter> filters)
bool multiSelect, string initialFileName, IEnumerable<FileDialogFilter> 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<IntPtr, FileDialogFilter>();
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<string[]> 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<string> 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();
});
}

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

Loading…
Cancel
Save