Browse Source

Merge branch 'master' into treeview_double_click

pull/9598/head
Steven Kirk 4 years ago
committed by GitHub
parent
commit
7d298aa579
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      .ncrunch/ReactiveUIDemo.v3.ncrunchproject
  2. 1
      Avalonia.Desktop.slnf
  3. 7
      Avalonia.sln
  4. 9
      native/Avalonia.Native/src/OSX/WindowImpl.mm
  5. 3
      samples/ControlCatalog/MainView.xaml
  6. 27
      samples/ControlCatalog/Pages/RefreshContainerPage.axaml
  7. 36
      samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs
  8. 26
      samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs
  9. 15
      samples/IntegrationTestApp/MainWindow.axaml
  10. 3
      samples/IntegrationTestApp/MainWindow.axaml.cs
  11. 8
      samples/IntegrationTestApp/ShowWindowTest.axaml
  12. 8
      samples/ReactiveUIDemo/App.axaml
  13. 37
      samples/ReactiveUIDemo/App.axaml.cs
  14. 19
      samples/ReactiveUIDemo/MainWindow.axaml
  15. 22
      samples/ReactiveUIDemo/MainWindow.axaml.cs
  16. 28
      samples/ReactiveUIDemo/ReactiveUIDemo.csproj
  17. 11
      samples/ReactiveUIDemo/ViewModels/BarViewModel.cs
  18. 11
      samples/ReactiveUIDemo/ViewModels/FooViewModel.cs
  19. 9
      samples/ReactiveUIDemo/ViewModels/MainWindowViewModel.cs
  20. 21
      samples/ReactiveUIDemo/ViewModels/RoutedViewHostPageViewModel.cs
  21. 16
      samples/ReactiveUIDemo/Views/BarView.axaml
  22. 28
      samples/ReactiveUIDemo/Views/BarView.axaml.cs
  23. 16
      samples/ReactiveUIDemo/Views/FooView.axaml
  24. 28
      samples/ReactiveUIDemo/Views/FooView.axaml.cs
  25. 3
      src/Avalonia.Base/Animation/Animators/Animator`1.cs
  26. 152
      src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs
  27. 8
      src/Avalonia.Base/Input/Gestures.cs
  28. 5
      src/Avalonia.Base/Input/InputElement.cs
  29. 43
      src/Avalonia.Base/Input/PullGestureEventArgs.cs
  30. 4
      src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs
  31. 67
      src/Avalonia.Base/StyledElement.cs
  32. 3
      src/Avalonia.Base/Styling/StyleInstance.cs
  33. 90
      src/Avalonia.Base/Visual.cs
  34. 6
      src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs
  35. 4
      src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
  36. 5
      src/Avalonia.Controls/ComboBox.cs
  37. 85
      src/Avalonia.Controls/Control.cs
  38. 2
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  39. 36
      src/Avalonia.Controls/PullToRefresh/RefreshCompletionDeferral.cs
  40. 252
      src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs
  41. 141
      src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs
  42. 42
      src/Avalonia.Controls/PullToRefresh/RefreshRequestedEventArgs.cs
  43. 553
      src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs
  44. 13
      src/Avalonia.Controls/PullToRefresh/RefreshVisualizerOrientation.cs
  45. 14
      src/Avalonia.Controls/PullToRefresh/RefreshVisualizerState.cs
  46. 274
      src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs
  47. 6
      src/Avalonia.Controls/TopLevel.cs
  48. 267
      src/Avalonia.Controls/Window.cs
  49. 58
      src/Avalonia.Controls/WindowBase.cs
  50. 4
      src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml
  51. 6
      src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml
  52. 2
      src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml
  53. 24
      src/Avalonia.Themes.Fluent/Controls/RefreshContainer.xaml
  54. 29
      src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml
  55. 3
      src/Avalonia.Themes.Simple/Accents/BaseDark.xaml
  56. 4
      src/Avalonia.Themes.Simple/Accents/BaseLight.xaml
  57. 24
      src/Avalonia.Themes.Simple/Controls/RefreshContainer.xaml
  58. 31
      src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml
  59. 2
      src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml
  60. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs
  61. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github
  62. 9
      src/Windows/Avalonia.Win32/TrayIconImpl.cs
  63. 5
      tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs
  64. 27
      tests/Avalonia.Base.UnitTests/FlowDirectionTests.cs
  65. 29
      tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs
  66. 48
      tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs
  67. 51
      tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs
  68. 22
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  69. 47
      tests/Avalonia.Controls.UnitTests/WindowTests.cs
  70. 16
      tests/Avalonia.IntegrationTests.Appium/WindowTests.cs
  71. 74
      tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs
  72. 23
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs

5
.ncrunch/ReactiveUIDemo.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

1
Avalonia.Desktop.slnf

@ -9,6 +9,7 @@
"samples\\MiniMvvm\\MiniMvvm.csproj",
"samples\\SampleControls\\ControlSamples.csproj",
"samples\\Sandbox\\Sandbox.csproj",
"samples\\ReactiveUIDemo\\ReactiveUIDemo.csproj",
"src\\Avalonia.Base\\Avalonia.Base.csproj",
"src\\Avalonia.Build.Tasks\\Avalonia.Build.Tasks.csproj",
"src\\Avalonia.Controls.ColorPicker\\Avalonia.Controls.ColorPicker.csproj",

7
Avalonia.sln

@ -232,6 +232,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Browser", "s
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ControlCatalog.Browser.Blazor", "samples\ControlCatalog.Browser.Blazor\ControlCatalog.Browser.Blazor.csproj", "{90B08091-9BBD-4362-B712-E9F2CC62B218}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\ReactiveUIDemo\ReactiveUIDemo.csproj", "{75C47156-C5D8-44BC-A5A7-E8657C2248D6}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -543,6 +545,10 @@ Global
{90B08091-9BBD-4362-B712-E9F2CC62B218}.Debug|Any CPU.Build.0 = Debug|Any CPU
{90B08091-9BBD-4362-B712-E9F2CC62B218}.Release|Any CPU.ActiveCfg = Release|Any CPU
{90B08091-9BBD-4362-B712-E9F2CC62B218}.Release|Any CPU.Build.0 = Release|Any CPU
{75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
{75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
{75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@ -607,6 +613,7 @@ Global
{47F8530C-F19B-4B1A-B4D6-EB231522AE5D} = {86A3F706-DC3C-43C6-BE1B-B98F5BAAA268}
{15B93A4C-1B46-43F6-B534-7B25B6E99932} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{90B08091-9BBD-4362-B712-E9F2CC62B218} = {9B9E3891-2366-4253-A952-D08BCEB71098}
{75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A}

9
native/Avalonia.Native/src/OSX/WindowImpl.mm

@ -137,7 +137,11 @@ void WindowImpl::BringToFront()
for(auto iterator = _children.begin(); iterator != _children.end(); iterator++)
{
(*iterator)->BringToFront();
auto window = (*iterator)->Window;
// #9565: Only bring window to front if it's on the currently active space
if ([window isOnActiveSpace])
(*iterator)->BringToFront();
}
}
}
@ -161,6 +165,9 @@ void WindowImpl::StartStateTransition() {
void WindowImpl::EndStateTransition() {
_transitioningWindowState = false;
// Ensure correct order of child windows after fullscreen transition.
BringToFront();
}
SystemDecorations WindowImpl::Decorations() {

3
samples/ControlCatalog/MainView.xaml

@ -135,6 +135,9 @@
<TabItem Header="RadioButton">
<pages:RadioButtonPage />
</TabItem>
<TabItem Header="RefreshContainer">
<pages:RefreshContainerPage />
</TabItem>
<TabItem Header="RelativePanel">
<pages:RelativePanelPage />
</TabItem>

27
samples/ControlCatalog/Pages/RefreshContainerPage.axaml

@ -0,0 +1,27 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="using:ControlCatalog.ViewModels"
mc:Ignorable="d"
d:DesignWidth="800"
d:DesignHeight="450"
x:DataType="viewModels:RefreshContainerViewModel"
x:Class="ControlCatalog.Pages.RefreshContainerPage">
<DockPanel HorizontalAlignment="Stretch"
Height="600"
VerticalAlignment="Top">
<Label DockPanel.Dock="Top">A control that supports pull to refresh</Label>
<RefreshContainer Name="Refresh"
DockPanel.Dock="Bottom"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
PullDirection="TopToBottom"
RefreshRequested="RefreshContainerPage_RefreshRequested"
Margin="5">
<ListBox HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Items="{Binding Items}"/>
</RefreshContainer>
</DockPanel>
</UserControl>

36
samples/ControlCatalog/Pages/RefreshContainerPage.axaml.cs

@ -0,0 +1,36 @@
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
public class RefreshContainerPage : UserControl
{
private RefreshContainerViewModel _viewModel;
public RefreshContainerPage()
{
this.InitializeComponent();
_viewModel = new RefreshContainerViewModel();
DataContext = _viewModel;
}
private async void RefreshContainerPage_RefreshRequested(object? sender, RefreshRequestedEventArgs e)
{
var deferral = e.GetDeferral();
await _viewModel.AddToTop();
deferral.Complete();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

26
samples/ControlCatalog/ViewModels/RefreshContainerViewModel.cs

@ -0,0 +1,26 @@
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using System.Threading.Tasks;
using Avalonia.Controls.Notifications;
using ControlCatalog.Pages;
using MiniMvvm;
namespace ControlCatalog.ViewModels
{
public class RefreshContainerViewModel : ViewModelBase
{
public ObservableCollection<string> Items { get; }
public RefreshContainerViewModel()
{
Items = new ObservableCollection<string>(Enumerable.Range(1, 200).Select(i => $"Item {i}"));
}
public async Task AddToTop()
{
await Task.Delay(3000);
Items.Insert(0, $"Item {200 - Items.Count}");
}
}
}

15
samples/IntegrationTestApp/MainWindow.axaml

@ -17,11 +17,15 @@
</NativeMenuItem>
<NativeMenuItem Header="View">
<NativeMenu/>
</NativeMenuItem>
</NativeMenuItem>
</NativeMenu>
</NativeMenu.Menu>
<DockPanel>
<NativeMenuBar DockPanel.Dock="Top"/>
<StackPanel DockPanel.Dock="Bottom" Margin="4" Orientation="Horizontal">
<TextBlock Margin="0,0,4,0">WindowState:</TextBlock>
<TextBlock Name="MainWindowState" Text="{Binding WindowState}"/>
</StackPanel>
<TabControl TabStripPlacement="Left" Name="MainTabs">
<TabItem Header="Automation">
@ -129,13 +133,14 @@
<ComboBoxItem>CenterOwner</ComboBoxItem>
</ComboBox>
<ComboBox Name="ShowWindowState" SelectedIndex="0">
<ComboBoxItem>Normal</ComboBoxItem>
<ComboBoxItem>Minimized</ComboBoxItem>
<ComboBoxItem>Maximized</ComboBoxItem>
<ComboBoxItem>FullScreen</ComboBoxItem>
<ComboBoxItem Name="ShowWindowStateNormal">Normal</ComboBoxItem>
<ComboBoxItem Name="ShowWindowStateMinimized">Minimized</ComboBoxItem>
<ComboBoxItem Name="ShowWindowStateMaximized">Maximized</ComboBoxItem>
<ComboBoxItem Name="ShowWindowStateFullScreen">FullScreen</ComboBoxItem>
</ComboBox>
<Button Name="ShowWindow">Show Window</Button>
<Button Name="SendToBack">Send to Back</Button>
<Button Name="EnterFullscreen">Enter Fullscreen</Button>
<Button Name="ExitFullscreen">Exit Fullscreen</Button>
<Button Name="RestoreAll">Restore All</Button>
</StackPanel>

3
samples/IntegrationTestApp/MainWindow.axaml.cs

@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia;
@ -178,6 +179,8 @@ namespace IntegrationTestApp
ShowWindow();
if (source?.Name == "SendToBack")
SendToBack();
if (source?.Name == "EnterFullscreen")
WindowState = WindowState.FullScreen;
if (source?.Name == "ExitFullscreen")
WindowState = WindowState.Normal;
if (source?.Name == "RestoreAll")

8
samples/IntegrationTestApp/ShowWindowTest.axaml

@ -27,10 +27,10 @@
<Label Grid.Column="0" Grid.Row="7">WindowState</Label>
<ComboBox Name="WindowState" Grid.Column="1" Grid.Row="7" SelectedIndex="{Binding WindowState}">
<ComboBoxItem>Normal</ComboBoxItem>
<ComboBoxItem>Minimized</ComboBoxItem>
<ComboBoxItem>Maximized</ComboBoxItem>
<ComboBoxItem>FullScreen</ComboBoxItem>
<ComboBoxItem Name="WindowStateNormal">Normal</ComboBoxItem>
<ComboBoxItem Name="WindowStateMinimized">Minimized</ComboBoxItem>
<ComboBoxItem Name="WindowStateMaximized">Maximized</ComboBoxItem>
<ComboBoxItem Name="WindowStateFullScreen">FullScreen</ComboBoxItem>
</ComboBox>
<Label Grid.Column="0" Grid.Row="8">Order (mac)</Label>

8
samples/ReactiveUIDemo/App.axaml

@ -0,0 +1,8 @@
<Application
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ReactiveUIDemo.App">
<Application.Styles>
<FluentTheme />
</Application.Styles>
</Application>

37
samples/ReactiveUIDemo/App.axaml.cs

@ -0,0 +1,37 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using ReactiveUI;
using ReactiveUIDemo.ViewModels;
using ReactiveUIDemo.Views;
using Splat;
namespace ReactiveUIDemo
{
public class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
Locator.CurrentMutable.Register(() => new FooView(), typeof(IViewFor<FooViewModel>));
Locator.CurrentMutable.Register(() => new BarView(), typeof(IViewFor<BarViewModel>));
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
desktop.MainWindow = new MainWindow();
base.OnFrameworkInitializationCompleted();
}
public static int Main(string[] args)
=> BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
.UsePlatformDetect()
.UseReactiveUI()
.LogToTrace();
}
}

19
samples/ReactiveUIDemo/MainWindow.axaml

@ -0,0 +1,19 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
x:Class="ReactiveUIDemo.MainWindow"
xmlns:vm="using:ReactiveUIDemo.ViewModels"
xmlns:rxui="using:Avalonia.ReactiveUI"
Title="AvaloniaUI ReactiveUI Demo"
x:DataType="vm:MainWindowViewModel">
<TabControl TabStripPlacement="Left">
<TabItem Header="RoutedViewHost">
<DockPanel DataContext="{Binding RoutedViewHost}">
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Spacing="8">
<Button Command="{Binding ShowFoo}">Foo</Button>
<Button Command="{Binding ShowBar}">Bar</Button>
</StackPanel>
<rxui:RoutedViewHost Router="{Binding Router}"/>
</DockPanel>
</TabItem>
</TabControl>
</Window>

22
samples/ReactiveUIDemo/MainWindow.axaml.cs

@ -0,0 +1,22 @@
using ReactiveUIDemo.ViewModels;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace ReactiveUIDemo
{
public class MainWindow : Window
{
public MainWindow()
{
this.InitializeComponent();
this.DataContext = new MainWindowViewModel();
this.AttachDevTools();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

28
samples/ReactiveUIDemo/ReactiveUIDemo.csproj

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Avalonia.ReactiveUI\Avalonia.ReactiveUI.csproj" />
<ProjectReference Include="..\..\src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Views\BarView.axaml.cs">
<DependentUpon>BarView.axaml</DependentUpon>
</Compile>
<Compile Update="Views\FooView.axaml.cs">
<DependentUpon>FooView.axaml</DependentUpon>
</Compile>
</ItemGroup>
<Import Project="..\..\build\SampleApp.props" />
<Import Project="..\..\build\ReferenceCoreLibraries.props" />
<Import Project="..\..\build\BuildTargets.targets" />
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\ReactiveUI.props" />
</Project>

11
samples/ReactiveUIDemo/ViewModels/BarViewModel.cs

@ -0,0 +1,11 @@
using ReactiveUI;
namespace ReactiveUIDemo.ViewModels
{
internal class BarViewModel : ReactiveObject, IRoutableViewModel
{
public BarViewModel(IScreen screen) => HostScreen = screen;
public string UrlPathSegment => "Bar";
public IScreen HostScreen { get; }
}
}

11
samples/ReactiveUIDemo/ViewModels/FooViewModel.cs

@ -0,0 +1,11 @@
using ReactiveUI;
namespace ReactiveUIDemo.ViewModels
{
internal class FooViewModel : ReactiveObject, IRoutableViewModel
{
public FooViewModel(IScreen screen) => HostScreen = screen;
public string UrlPathSegment => "Foo";
public IScreen HostScreen { get; }
}
}

9
samples/ReactiveUIDemo/ViewModels/MainWindowViewModel.cs

@ -0,0 +1,9 @@
using ReactiveUI;
namespace ReactiveUIDemo.ViewModels
{
internal class MainWindowViewModel : ReactiveObject
{
public RoutedViewHostPageViewModel RoutedViewHost { get; } = new();
}
}

21
samples/ReactiveUIDemo/ViewModels/RoutedViewHostPageViewModel.cs

@ -0,0 +1,21 @@
using ReactiveUI;
namespace ReactiveUIDemo.ViewModels
{
internal class RoutedViewHostPageViewModel : ReactiveObject, IScreen
{
public RoutedViewHostPageViewModel()
{
Foo = new(this);
Bar = new(this);
Router.Navigate.Execute(Foo);
}
public RoutingState Router { get; } = new();
public FooViewModel Foo { get; }
public BarViewModel Bar { get; }
public void ShowFoo() => Router.Navigate.Execute(Foo);
public void ShowBar() => Router.Navigate.Execute(Bar);
}
}

16
samples/ReactiveUIDemo/Views/BarView.axaml

@ -0,0 +1,16 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="ReactiveUIDemo.Views.BarView"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Border Background="Blue">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="48">
Bar!
</TextBlock>
</Border>
</UserControl>

28
samples/ReactiveUIDemo/Views/BarView.axaml.cs

@ -0,0 +1,28 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using ReactiveUI;
using ReactiveUIDemo.ViewModels;
namespace ReactiveUIDemo.Views
{
internal partial class BarView : UserControl, IViewFor<BarViewModel>
{
public BarView()
{
InitializeComponent();
}
public BarViewModel? ViewModel { get; set; }
object? IViewFor.ViewModel
{
get => ViewModel;
set => ViewModel = (BarViewModel?)value;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

16
samples/ReactiveUIDemo/Views/FooView.axaml

@ -0,0 +1,16 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="ReactiveUIDemo.Views.FooView"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Border Background="Red">
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
FontSize="48">
Foo!
</TextBlock>
</Border>
</UserControl>

28
samples/ReactiveUIDemo/Views/FooView.axaml.cs

@ -0,0 +1,28 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using ReactiveUI;
using ReactiveUIDemo.ViewModels;
namespace ReactiveUIDemo.Views
{
internal partial class FooView : UserControl, IViewFor<FooViewModel>
{
public FooView()
{
InitializeComponent();
}
public FooViewModel? ViewModel { get; set; }
object? IViewFor.ViewModel
{
get => ViewModel;
set => ViewModel = (FooViewModel?)value;
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

3
src/Avalonia.Base/Animation/Animators/Animator`1.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia.Animation.Utils;
using Avalonia.Collections;
@ -39,7 +40,7 @@ namespace Avalonia.Animation.Animators
VerifyConvertKeyFrames();
var subject = new DisposeAnimationInstanceSubject<T>(this, animation, control, clock, onComplete);
return match.Subscribe(subject);
return new CompositeDisposable(match.Subscribe(subject), subject);
}
protected T InterpolationHandler(double animationTime, T neutralValue)

152
src/Avalonia.Base/Input/GestureRecognizers/PullGestureRecognizer.cs

@ -0,0 +1,152 @@
using Avalonia.Input.GestureRecognizers;
namespace Avalonia.Input
{
public class PullGestureRecognizer : StyledElement, IGestureRecognizer
{
private IInputElement? _target;
private IGestureRecognizerActionsDispatcher? _actions;
private Point _initialPosition;
private int _gestureId;
private IPointer? _tracking;
private PullDirection _pullDirection;
/// <summary>
/// Defines the <see cref="PullDirection"/> property.
/// </summary>
public static readonly DirectProperty<PullGestureRecognizer, PullDirection> PullDirectionProperty =
AvaloniaProperty.RegisterDirect<PullGestureRecognizer, PullDirection>(
nameof(PullDirection),
o => o.PullDirection,
(o, v) => o.PullDirection = v);
public PullDirection PullDirection
{
get => _pullDirection;
set => SetAndRaise(PullDirectionProperty, ref _pullDirection, value);
}
public PullGestureRecognizer(PullDirection pullDirection)
{
PullDirection = pullDirection;
}
public void Initialize(IInputElement target, IGestureRecognizerActionsDispatcher actions)
{
_target = target;
_actions = actions;
_target?.AddHandler(InputElement.PointerPressedEvent, OnPointerPressed, Interactivity.RoutingStrategies.Tunnel | Interactivity.RoutingStrategies.Bubble);
_target?.AddHandler(InputElement.PointerReleasedEvent, OnPointerReleased, Interactivity.RoutingStrategies.Tunnel | Interactivity.RoutingStrategies.Bubble);
}
private void OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
PointerPressed(e);
}
private void OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
PointerReleased(e);
}
public void PointerCaptureLost(IPointer pointer)
{
if (_tracking == pointer)
{
EndPull();
}
}
public void PointerMoved(PointerEventArgs e)
{
if (_tracking == e.Pointer && _target is Visual visual)
{
var currentPosition = e.GetPosition(visual);
_actions!.Capture(e.Pointer, this);
Vector delta = default;
switch (PullDirection)
{
case PullDirection.TopToBottom:
if (currentPosition.Y > _initialPosition.Y)
{
delta = new Vector(0, currentPosition.Y - _initialPosition.Y);
}
break;
case PullDirection.BottomToTop:
if (currentPosition.Y < _initialPosition.Y)
{
delta = new Vector(0, _initialPosition.Y - currentPosition.Y);
}
break;
case PullDirection.LeftToRight:
if (currentPosition.X > _initialPosition.X)
{
delta = new Vector(currentPosition.X - _initialPosition.X, 0);
}
break;
case PullDirection.RightToLeft:
if (currentPosition.X < _initialPosition.X)
{
delta = new Vector(_initialPosition.X - currentPosition.X, 0);
}
break;
}
_target?.RaiseEvent(new PullGestureEventArgs(_gestureId, delta, PullDirection));
}
}
public void PointerPressed(PointerPressedEventArgs e)
{
if (_target != null && _target is Visual visual && (e.Pointer.Type == PointerType.Touch || e.Pointer.Type == PointerType.Pen))
{
var position = e.GetPosition(visual);
var canPull = false;
var bounds = visual.Bounds;
switch (PullDirection)
{
case PullDirection.TopToBottom:
canPull = position.Y < bounds.Height * 0.1;
break;
case PullDirection.BottomToTop:
canPull = position.Y > bounds.Height - (bounds.Height * 0.1);
break;
case PullDirection.LeftToRight:
canPull = position.X < bounds.Width * 0.1;
break;
case PullDirection.RightToLeft:
canPull = position.X > bounds.Width - (bounds.Width * 0.1);
break;
}
if (canPull)
{
_gestureId = PullGestureEventArgs.GetNextFreeId();
_tracking = e.Pointer;
_initialPosition = position;
}
}
}
public void PointerReleased(PointerReleasedEventArgs e)
{
if (_tracking == e.Pointer)
{
EndPull();
}
}
private void EndPull()
{
_tracking = null;
_initialPosition = default;
_target?.RaiseEvent(new PullGestureEndedEventArgs(_gestureId, PullDirection));
}
}
}

8
src/Avalonia.Base/Input/Gestures.cs

@ -46,6 +46,14 @@ namespace Avalonia.Input
private static readonly WeakReference<object?> s_lastPress = new WeakReference<object?>(null);
private static Point s_lastPressPoint;
public static readonly RoutedEvent<PullGestureEventArgs> PullGestureEvent =
RoutedEvent.Register<PullGestureEventArgs>(
"PullGesture", RoutingStrategies.Bubble, typeof(Gestures));
public static readonly RoutedEvent<PullGestureEndedEventArgs> PullGestureEndedEvent =
RoutedEvent.Register<PullGestureEndedEventArgs>(
"PullGestureEnded", RoutingStrategies.Bubble, typeof(Gestures));
static Gestures()
{
InputElement.PointerPressedEvent.RouteFinished.Subscribe(PointerPressed);

5
src/Avalonia.Base/Input/InputElement.cs

@ -442,6 +442,11 @@ namespace Avalonia.Input
{
SetAndRaise(IsEffectivelyEnabledProperty, ref _isEffectivelyEnabled, value);
PseudoClasses.Set(":disabled", !value);
if (!IsEffectivelyEnabled && FocusManager.Instance?.Current == this)
{
FocusManager.Instance?.Focus(null);
}
}
}

43
src/Avalonia.Base/Input/PullGestureEventArgs.cs

@ -0,0 +1,43 @@
using System;
using Avalonia.Interactivity;
namespace Avalonia.Input
{
public class PullGestureEventArgs : RoutedEventArgs
{
public int Id { get; }
public Vector Delta { get; }
public PullDirection PullDirection { get; }
private static int _nextId = 1;
internal static int GetNextFreeId() => _nextId++;
public PullGestureEventArgs(int id, Vector delta, PullDirection pullDirection) : base(Gestures.PullGestureEvent)
{
Id = id;
Delta = delta;
PullDirection = pullDirection;
}
}
public class PullGestureEndedEventArgs : RoutedEventArgs
{
public int Id { get; }
public PullDirection PullDirection { get; }
public PullGestureEndedEventArgs(int id, PullDirection pullDirection) : base(Gestures.PullGestureEndedEvent)
{
Id = id;
PullDirection = pullDirection;
}
}
public enum PullDirection
{
TopToBottom,
BottomToTop,
LeftToRight,
RightToLeft
}
}

4
src/Avalonia.Base/Rendering/Composition/Animations/KeyFrameAnimationInstance.cs

@ -175,6 +175,10 @@ namespace Avalonia.Rendering.Composition.Animations
public override void Activate()
{
if (_finished)
{
return;
}
TargetObject.Compositor.AddToClock(this);
base.Activate();
}

67
src/Avalonia.Base/StyledElement.cs

@ -81,6 +81,7 @@ namespace Avalonia
private Styles? _styles;
private bool _stylesApplied;
private bool _themeApplied;
private bool _templatedParentThemeApplied;
private AvaloniaObject? _templatedParent;
private bool _dataContextUpdating;
private ControlTheme? _implicitTheme;
@ -375,6 +376,12 @@ namespace Avalonia
_themeApplied = true;
}
if (!_templatedParentThemeApplied)
{
ApplyTemplatedParentControlTheme();
_templatedParentThemeApplied = true;
}
if (!_stylesApplied)
{
ApplyStyles(this);
@ -613,26 +620,38 @@ namespace Avalonia
base.OnPropertyChanged(change);
if (change.Property == ThemeProperty)
{
OnControlThemeChanged();
_themeApplied = false;
}
}
private protected virtual void OnControlThemeChanged()
{
var values = GetValueStore();
values.BeginStyling();
try { values.RemoveFrames(FrameType.Theme); }
finally { values.EndStyling(); }
try
{
values.RemoveFrames(FrameType.Theme);
}
finally
{
values.EndStyling();
_themeApplied = false;
}
}
internal virtual void OnTemplatedParentControlThemeChanged()
{
var values = GetValueStore();
values.BeginStyling();
try { values.RemoveFrames(FrameType.TemplatedParentTheme); }
finally { values.EndStyling(); }
try
{
values.RemoveFrames(FrameType.TemplatedParentTheme);
}
finally
{
values.EndStyling();
_templatedParentThemeApplied = false;
}
}
internal ControlTheme? GetEffectiveTheme()
@ -743,13 +762,13 @@ namespace Avalonia
private void ApplyControlTheme()
{
var theme = GetEffectiveTheme();
if (theme is not null)
if (GetEffectiveTheme() is { } theme)
ApplyControlTheme(theme, FrameType.Theme);
}
if (TemplatedParent is StyledElement styleableParent &&
styleableParent.GetEffectiveTheme() is { } parentTheme)
private void ApplyTemplatedParentControlTheme()
{
if ((TemplatedParent as StyledElement)?.GetEffectiveTheme() is { } parentTheme)
{
ApplyControlTheme(parentTheme, FrameType.TemplatedParentTheme);
}
@ -793,6 +812,28 @@ namespace Avalonia
ApplyStyle(child, host, type);
}
private void ReevaluateImplicitTheme()
{
// We only need to check if the theme has changed when Theme isn't set (i.e. when we
// have an implicit theme).
if (Theme is not null)
return;
// Refetch the implicit theme.
var oldImplicitTheme = _implicitTheme == s_invalidTheme ? null : _implicitTheme;
_implicitTheme = null;
GetEffectiveTheme();
var newImplicitTheme = _implicitTheme == s_invalidTheme ? null : _implicitTheme;
// If the implicit theme has changed, detach the existing theme.
if (newImplicitTheme != oldImplicitTheme)
{
OnControlThemeChanged();
_themeApplied = false;
}
}
private void OnAttachedToLogicalTreeCore(LogicalTreeAttachmentEventArgs e)
{
if (this.GetLogicalParent() == null && !(this is ILogicalRoot))
@ -811,6 +852,7 @@ namespace Avalonia
{
_logicalRoot = e.Root;
ReevaluateImplicitTheme();
ApplyStyling();
NotifyResourcesChanged(propagate: false);
@ -835,7 +877,6 @@ namespace Avalonia
if (_logicalRoot != null)
{
_logicalRoot = null;
_implicitTheme = null;
InvalidateStyles(recurse: false);
OnDetachedFromLogicalTree(e);
DetachedFromLogicalTree?.Invoke(this, e);

3
src/Avalonia.Base/Styling/StyleInstance.cs

@ -70,6 +70,9 @@ namespace Avalonia.Styling
_animationTrigger ??= new Subject<bool>();
foreach (var animation in _animations)
animation.Apply(animatable, null, _animationTrigger);
if (_activator is null)
_animationTrigger.OnNext(true);
}
}

90
src/Avalonia.Base/Visual.cs

@ -89,6 +89,14 @@ namespace Avalonia
public static readonly StyledProperty<RelativePoint> RenderTransformOriginProperty =
AvaloniaProperty.Register<Visual, RelativePoint>(nameof(RenderTransformOrigin), defaultValue: RelativePoint.Center);
/// <summary>
/// Defines the <see cref="FlowDirection"/> property.
/// </summary>
public static readonly AttachedProperty<FlowDirection> FlowDirectionProperty =
AvaloniaProperty.RegisterAttached<Visual, Visual, FlowDirection>(
nameof(FlowDirection),
inherits: true);
/// <summary>
/// Defines the <see cref="VisualParent"/> property.
/// </summary>
@ -263,6 +271,15 @@ namespace Avalonia
set { SetValue(RenderTransformOriginProperty, value); }
}
/// <summary>
/// Gets or sets the text flow direction.
/// </summary>
public FlowDirection FlowDirection
{
get => GetValue(FlowDirectionProperty);
set => SetValue(FlowDirectionProperty, value);
}
/// <summary>
/// Gets or sets the Z index of the control.
/// </summary>
@ -306,6 +323,36 @@ namespace Avalonia
/// </summary>
internal Visual? VisualParent => _visualParent;
/// <summary>
/// Gets a value indicating whether control bypass FlowDirecton policies.
/// </summary>
/// <remarks>
/// Related to FlowDirection system and returns false as default, so if
/// <see cref="FlowDirection"/> is RTL then control will get a mirror presentation.
/// For controls that want to avoid this behavior, override this property and return true.
/// </remarks>
protected virtual bool BypassFlowDirectionPolicies => false;
/// <summary>
/// Gets the value of the attached <see cref="FlowDirectionProperty"/> on a control.
/// </summary>
/// <param name="visual">The control.</param>
/// <returns>The flow direction.</returns>
public static FlowDirection GetFlowDirection(Visual visual)
{
return visual.GetValue(FlowDirectionProperty);
}
/// <summary>
/// Sets the value of the attached <see cref="FlowDirectionProperty"/> on a control.
/// </summary>
/// <param name="visual">The control.</param>
/// <param name="value">The property value to set.</param>
public static void SetFlowDirection(Visual visual, FlowDirection value)
{
visual.SetValue(FlowDirectionProperty, value);
}
/// <summary>
/// Invalidates the visual and queues a repaint.
/// </summary>
@ -387,6 +434,22 @@ namespace Avalonia
}
}
/// <inheritdoc/>
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == FlowDirectionProperty)
{
InvalidateMirrorTransform();
foreach (var child in VisualChildren)
{
child.InvalidateMirrorTransform();
}
}
}
protected override void LogicalChildrenCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
base.LogicalChildrenCollectionChanged(sender, e);
@ -682,5 +745,32 @@ namespace Avalonia
visual.SetVisualParent(parent);
}
}
/// <summary>
/// Computes the <see cref="HasMirrorTransform"/> value according to the
/// <see cref="FlowDirection"/> and <see cref="BypassFlowDirectionPolicies"/>
/// </summary>
public virtual void InvalidateMirrorTransform()
{
var flowDirection = this.FlowDirection;
var parentFlowDirection = FlowDirection.LeftToRight;
bool bypassFlowDirectionPolicies = BypassFlowDirectionPolicies;
bool parentBypassFlowDirectionPolicies = false;
var parent = VisualParent;
if (parent != null)
{
parentFlowDirection = parent.FlowDirection;
parentBypassFlowDirectionPolicies = parent.BypassFlowDirectionPolicies;
}
bool thisShouldBeMirrored = flowDirection == FlowDirection.RightToLeft && !bypassFlowDirectionPolicies;
bool parentShouldBeMirrored = parentFlowDirection == FlowDirection.RightToLeft && !parentBypassFlowDirectionPolicies;
bool shouldApplyMirrorTransform = thisShouldBeMirrored != parentShouldBeMirrored;
HasMirrorTransform = shouldApplyMirrorTransform;
}
}
}

6
src/Avalonia.Controls.DataGrid/DataGridTemplateColumn.cs

@ -64,9 +64,11 @@ namespace Avalonia.Controls
protected override Control GenerateElement(DataGridCell cell, object dataItem)
{
if(CellTemplate != null)
if (CellTemplate != null)
{
return CellTemplate.Build(dataItem);
return (CellTemplate is IRecyclingDataTemplate recyclingDataTemplate)
? recyclingDataTemplate.Build(dataItem, cell.Content as Control)
: CellTemplate.Build(dataItem);
}
if (Design.IsDesignMode)
{

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

@ -6,8 +6,8 @@
<StreamGeometry x:Key="DataGridSortIconDescendingPath">M1875 1011l-787 787v-1798h-128v1798l-787 -787l-90 90l941 941l941 -941z</StreamGeometry>
<StreamGeometry x:Key="DataGridSortIconAscendingPath">M1965 947l-941 -941l-941 941l90 90l787 -787v1798h128v-1798l787 787z</StreamGeometry>
<StreamGeometry x:Key="DataGridRowGroupHeaderIconClosedPath">M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z</StreamGeometry>
<StreamGeometry x:Key="DataGridRowGroupHeaderIconOpenedPath">M109 486 19 576 1024 1581 2029 576 1939 486 1024 1401z</StreamGeometry>
<StreamGeometry x:Key="DataGridRowGroupHeaderIconOpenedPath">M515 93l930 931l-930 931l90 90l1022 -1021l-1022 -1021z</StreamGeometry>
<StreamGeometry x:Key="DataGridRowGroupHeaderIconClosedPath">M109 486 19 576 1024 1581 2029 576 1939 486 1024 1401z</StreamGeometry>
<SolidColorBrush x:Key="DataGridColumnHeaderForegroundBrush" Color="{DynamicResource SystemBaseMediumColor}" />
<SolidColorBrush x:Key="DataGridColumnHeaderBackgroundBrush" Color="{DynamicResource SystemAltHighColor}" />

5
src/Avalonia.Controls/ComboBox.cs

@ -454,10 +454,9 @@ namespace Avalonia.Controls
{
if (SelectionBoxItem is Rectangle rectangle)
{
if ((rectangle.Fill as VisualBrush)?.Visual is Control content)
if ((rectangle.Fill as VisualBrush)?.Visual is Visual content)
{
var flowDirection = (((Visual)content!).VisualParent as Control)?.FlowDirection ??
FlowDirection.LeftToRight;
var flowDirection = content.VisualParent?.FlowDirection ?? FlowDirection.LeftToRight;
rectangle.FlowDirection = flowDirection;
}
}

85
src/Avalonia.Controls/Control.cs

@ -91,13 +91,6 @@ namespace Avalonia.Controls
RoutedEvent.Register<Control, SizeChangedEventArgs>(
nameof(SizeChanged), RoutingStrategies.Direct);
/// <summary>
/// Defines the <see cref="FlowDirection"/> property.
/// </summary>
public static readonly AttachedProperty<FlowDirection> FlowDirectionProperty =
AvaloniaProperty.RegisterAttached<Control, Control, FlowDirection>(
nameof(FlowDirection),
inherits: true);
// Note the following:
// _loadedQueue :
@ -170,15 +163,6 @@ namespace Avalonia.Controls
get => GetValue(TagProperty);
set => SetValue(TagProperty, value);
}
/// <summary>
/// Gets or sets the text flow direction.
/// </summary>
public FlowDirection FlowDirection
{
get => GetValue(FlowDirectionProperty);
set => SetValue(FlowDirectionProperty, value);
}
/// <summary>
/// Occurs when the user has completed a context input gesture, such as a right-click.
@ -229,39 +213,9 @@ namespace Avalonia.Controls
public new Control? Parent => (Control?)base.Parent;
/// <summary>
/// Gets the value of the attached <see cref="FlowDirectionProperty"/> on a control.
/// </summary>
/// <param name="control">The control.</param>
/// <returns>The flow direction.</returns>
public static FlowDirection GetFlowDirection(Control control)
{
return control.GetValue(FlowDirectionProperty);
}
/// <summary>
/// Sets the value of the attached <see cref="FlowDirectionProperty"/> on a control.
/// </summary>
/// <param name="control">The control.</param>
/// <param name="value">The property value to set.</param>
public static void SetFlowDirection(Control control, FlowDirection value)
{
control.SetValue(FlowDirectionProperty, value);
}
/// <inheritdoc/>
bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null;
/// <summary>
/// Gets a value indicating whether control bypass FlowDirecton policies.
/// </summary>
/// <remarks>
/// Related to FlowDirection system and returns false as default, so if
/// <see cref="FlowDirection"/> is RTL then control will get a mirror presentation.
/// For controls that want to avoid this behavior, override this property and return true.
/// </remarks>
protected virtual bool BypassFlowDirectionPolicies => false;
/// <inheritdoc/>
void ISetterValue.Initialize(ISetter setter)
{
@ -571,45 +525,6 @@ namespace Avalonia.Controls
RaiseEvent(sizeChangedEventArgs);
}
}
else if (change.Property == FlowDirectionProperty)
{
InvalidateMirrorTransform();
foreach (var visual in VisualChildren)
{
if (visual is Control child)
{
child.InvalidateMirrorTransform();
}
}
}
}
/// <summary>
/// Computes the <see cref="Visual.HasMirrorTransform"/> value according to the
/// <see cref="FlowDirection"/> and <see cref="BypassFlowDirectionPolicies"/>
/// </summary>
public virtual void InvalidateMirrorTransform()
{
var flowDirection = this.FlowDirection;
var parentFlowDirection = FlowDirection.LeftToRight;
bool bypassFlowDirectionPolicies = BypassFlowDirectionPolicies;
bool parentBypassFlowDirectionPolicies = false;
var parent = this.VisualParent as Control;
if (parent != null)
{
parentFlowDirection = parent.FlowDirection;
parentBypassFlowDirectionPolicies = parent.BypassFlowDirectionPolicies;
}
bool thisShouldBeMirrored = flowDirection == FlowDirection.RightToLeft && !bypassFlowDirectionPolicies;
bool parentShouldBeMirrored = parentFlowDirection == FlowDirection.RightToLeft && !parentBypassFlowDirectionPolicies;
bool shouldApplyMirrorTransform = thisShouldBeMirrored != parentShouldBeMirrored;
HasMirrorTransform = shouldApplyMirrorTransform;
}
}
}

2
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -588,7 +588,7 @@ namespace Avalonia.Controls.Primitives
}
else if (e.Key == Key.Space || e.Key == Key.Enter)
{
e.Handled = UpdateSelectionFromEventSource(
UpdateSelectionFromEventSource(
e.Source,
true,
e.KeyModifiers.HasFlag(KeyModifiers.Shift),

36
src/Avalonia.Controls/PullToRefresh/RefreshCompletionDeferral.cs

@ -0,0 +1,36 @@
using System;
using System.Threading;
namespace Avalonia.Controls
{
/// <summary>
/// Deferral class for notify that a work done in RefreshRequested event is done.
/// </summary>
public class RefreshCompletionDeferral
{
private Action _deferredAction;
private int _deferCount;
public RefreshCompletionDeferral(Action deferredAction)
{
_deferredAction = deferredAction;
}
public void Complete()
{
Interlocked.Decrement(ref _deferCount);
if (_deferCount == 0)
{
_deferredAction?.Invoke();
}
}
public RefreshCompletionDeferral Get()
{
Interlocked.Increment(ref _deferCount);
return this;
}
}
}

252
src/Avalonia.Controls/PullToRefresh/RefreshContainer.cs

@ -0,0 +1,252 @@
using System;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.PullToRefresh;
using Avalonia.Input;
using Avalonia.Interactivity;
namespace Avalonia.Controls
{
/// <summary>
/// Represents a container control that provides a <see cref="RefreshVisualizer"/> and pull-to-refresh functionality for scrollable content.
/// </summary>
public class RefreshContainer : ContentControl
{
internal const int DefaultPullDimensionSize = 100;
private bool _hasDefaultRefreshInfoProviderAdapter;
private ScrollViewerIRefreshInfoProviderAdapter? _refreshInfoProviderAdapter;
private RefreshInfoProvider? _refreshInfoProvider;
private IDisposable? _visualizerSizeSubscription;
private Grid? _visualizerPresenter;
private RefreshVisualizer? _refreshVisualizer;
private bool _hasDefaultRefreshVisualizer;
/// <summary>
/// Defines the <see cref="RefreshRequested"/> event.
/// </summary>
public static readonly RoutedEvent<RefreshRequestedEventArgs> RefreshRequestedEvent =
RoutedEvent.Register<RefreshContainer, RefreshRequestedEventArgs>(nameof(RefreshRequested), RoutingStrategies.Bubble);
internal static readonly DirectProperty<RefreshContainer, ScrollViewerIRefreshInfoProviderAdapter?> RefreshInfoProviderAdapterProperty =
AvaloniaProperty.RegisterDirect<RefreshContainer, ScrollViewerIRefreshInfoProviderAdapter?>(nameof(RefreshInfoProviderAdapter),
(s) => s.RefreshInfoProviderAdapter, (s, o) => s.RefreshInfoProviderAdapter = o);
/// <summary>
/// Defines the <see cref="Visualizer"/> event.
/// </summary>
public static readonly DirectProperty<RefreshContainer, RefreshVisualizer?> VisualizerProperty =
AvaloniaProperty.RegisterDirect<RefreshContainer, RefreshVisualizer?>(nameof(Visualizer),
s => s.Visualizer, (s, o) => s.Visualizer = o);
/// <summary>
/// Defines the <see cref="PullDirection"/> event.
/// </summary>
public static readonly StyledProperty<PullDirection> PullDirectionProperty =
AvaloniaProperty.Register<RefreshContainer, PullDirection>(nameof(PullDirection), PullDirection.TopToBottom);
internal ScrollViewerIRefreshInfoProviderAdapter? RefreshInfoProviderAdapter
{
get => _refreshInfoProviderAdapter; set
{
_hasDefaultRefreshInfoProviderAdapter = false;
SetAndRaise(RefreshInfoProviderAdapterProperty, ref _refreshInfoProviderAdapter, value);
}
}
/// <summary>
/// Gets or sets the <see cref="RefreshVisualizer"/> for this container.
/// </summary>
public RefreshVisualizer? Visualizer
{
get => _refreshVisualizer; set
{
if (_refreshVisualizer != null)
{
_visualizerSizeSubscription?.Dispose();
_refreshVisualizer.RefreshRequested -= Visualizer_RefreshRequested;
}
SetAndRaise(VisualizerProperty, ref _refreshVisualizer, value);
}
}
/// <summary>
/// Gets or sets a value that specifies the direction to pull to initiate a refresh.
/// </summary>
public PullDirection PullDirection
{
get => GetValue(PullDirectionProperty);
set => SetValue(PullDirectionProperty, value);
}
/// <summary>
/// Occurs when an update of the content has been initiated.
/// </summary>
public event EventHandler<RefreshRequestedEventArgs>? RefreshRequested
{
add => AddHandler(RefreshRequestedEvent, value);
remove => RemoveHandler(RefreshRequestedEvent, value);
}
public RefreshContainer()
{
_hasDefaultRefreshInfoProviderAdapter = true;
_refreshInfoProviderAdapter = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection);
RaisePropertyChanged(RefreshInfoProviderAdapterProperty, null, _refreshInfoProviderAdapter);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_visualizerPresenter = e.NameScope.Find<Grid>("PART_RefreshVisualizerPresenter");
if (_refreshVisualizer == null)
{
_hasDefaultRefreshVisualizer = true;
Visualizer = new RefreshVisualizer();
}
else
{
_hasDefaultRefreshVisualizer = false;
RaisePropertyChanged(VisualizerProperty, default, _refreshVisualizer);
}
OnPullDirectionChanged();
}
private void OnVisualizerSizeChanged(Rect obj)
{
if (_hasDefaultRefreshInfoProviderAdapter)
{
_refreshInfoProviderAdapter = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection);
RaisePropertyChanged(RefreshInfoProviderAdapterProperty, null, _refreshInfoProviderAdapter);
}
}
private void Visualizer_RefreshRequested(object? sender, RefreshRequestedEventArgs e)
{
var ev = new RefreshRequestedEventArgs(e.GetDeferral(), RefreshRequestedEvent);
RaiseEvent(ev);
ev.DecrementCount();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == RefreshInfoProviderAdapterProperty)
{
if (_refreshVisualizer != null)
{
if (_refreshInfoProvider != null)
{
_refreshVisualizer.RefreshInfoProvider = _refreshInfoProvider;
}
else
{
if (RefreshInfoProviderAdapter != null && _refreshVisualizer != null)
{
_refreshInfoProvider = RefreshInfoProviderAdapter?.AdaptFromTree(this, _refreshVisualizer.Bounds.Size);
if (_refreshInfoProvider != null)
{
_refreshVisualizer.RefreshInfoProvider = _refreshInfoProvider;
RefreshInfoProviderAdapter?.SetAnimations(_refreshVisualizer);
}
}
}
}
}
else if (change.Property == VisualizerProperty)
{
if (_visualizerPresenter != null)
{
_visualizerPresenter.Children.Clear();
if (_refreshVisualizer != null)
{
_visualizerPresenter.Children.Add(_refreshVisualizer);
}
}
if (_refreshVisualizer != null)
{
_refreshVisualizer.RefreshRequested += Visualizer_RefreshRequested;
_visualizerSizeSubscription = _refreshVisualizer.GetObservable(Control.BoundsProperty).Subscribe(OnVisualizerSizeChanged);
}
}
else if (change.Property == PullDirectionProperty)
{
OnPullDirectionChanged();
}
}
private void OnPullDirectionChanged()
{
if (_visualizerPresenter != null && _refreshVisualizer != null)
{
switch (PullDirection)
{
case PullDirection.TopToBottom:
_visualizerPresenter.VerticalAlignment = Layout.VerticalAlignment.Top;
_visualizerPresenter.HorizontalAlignment = Layout.HorizontalAlignment.Stretch;
if (_hasDefaultRefreshVisualizer)
{
_refreshVisualizer.PullDirection = PullDirection.TopToBottom;
_refreshVisualizer.Height = DefaultPullDimensionSize;
_refreshVisualizer.Width = double.NaN;
}
break;
case PullDirection.BottomToTop:
_visualizerPresenter.VerticalAlignment = Layout.VerticalAlignment.Bottom;
_visualizerPresenter.HorizontalAlignment = Layout.HorizontalAlignment.Stretch;
if (_hasDefaultRefreshVisualizer)
{
_refreshVisualizer.PullDirection = PullDirection.BottomToTop;
_refreshVisualizer.Height = DefaultPullDimensionSize;
_refreshVisualizer.Width = double.NaN;
}
break;
case PullDirection.LeftToRight:
_visualizerPresenter.VerticalAlignment = Layout.VerticalAlignment.Stretch;
_visualizerPresenter.HorizontalAlignment = Layout.HorizontalAlignment.Left;
if (_hasDefaultRefreshVisualizer)
{
_refreshVisualizer.PullDirection = PullDirection.LeftToRight;
_refreshVisualizer.Width = DefaultPullDimensionSize;
_refreshVisualizer.Height = double.NaN;
}
break;
case PullDirection.RightToLeft:
_visualizerPresenter.VerticalAlignment = Layout.VerticalAlignment.Stretch;
_visualizerPresenter.HorizontalAlignment = Layout.HorizontalAlignment.Right;
if (_hasDefaultRefreshVisualizer)
{
_refreshVisualizer.PullDirection = PullDirection.RightToLeft;
_refreshVisualizer.Width = DefaultPullDimensionSize;
_refreshVisualizer.Height = double.NaN;
}
break;
}
if (_hasDefaultRefreshInfoProviderAdapter &&
_hasDefaultRefreshVisualizer &&
_refreshVisualizer.Bounds.Height == DefaultPullDimensionSize &&
_refreshVisualizer.Bounds.Width == DefaultPullDimensionSize)
{
_refreshInfoProviderAdapter = new ScrollViewerIRefreshInfoProviderAdapter(PullDirection);
RaisePropertyChanged(RefreshInfoProviderAdapterProperty, null, _refreshInfoProviderAdapter);
}
}
}
/// <summary>
/// Initiates an update of the content.
/// </summary>
public void RequestRefresh()
{
_refreshVisualizer?.RequestRefresh();
}
}
}

141
src/Avalonia.Controls/PullToRefresh/RefreshInfoProvider.cs

@ -0,0 +1,141 @@
using System;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Rendering.Composition;
namespace Avalonia.Controls.PullToRefresh
{
internal class RefreshInfoProvider : Interactive
{
internal const double DefaultExecutionRatio = 0.8;
private readonly PullDirection _refreshPullDirection;
private readonly Size _refreshVisualizerSize;
private readonly CompositionVisual? _visual;
private bool _isInteractingForRefresh;
private double _interactionRatio;
private bool _entered;
public DirectProperty<RefreshInfoProvider, bool> IsInteractingForRefreshProperty =
AvaloniaProperty.RegisterDirect<RefreshInfoProvider, bool>(nameof(IsInteractingForRefresh),
s => s.IsInteractingForRefresh, (s, o) => s.IsInteractingForRefresh = o);
public DirectProperty<RefreshInfoProvider, double> ExecutionRatioProperty =
AvaloniaProperty.RegisterDirect<RefreshInfoProvider, double>(nameof(ExecutionRatio),
s => s.ExecutionRatio);
public DirectProperty<RefreshInfoProvider, double> InteractionRatioProperty =
AvaloniaProperty.RegisterDirect<RefreshInfoProvider, double>(nameof(InteractionRatio),
s => s.InteractionRatio, (s, o) => s.InteractionRatio = o);
/// <summary>
/// Defines the <see cref="RefreshStarted"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> RefreshStartedEvent =
RoutedEvent.Register<RefreshInfoProvider, RoutedEventArgs>(nameof(RefreshStarted), RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="RefreshCompleted"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> RefreshCompletedEvent =
RoutedEvent.Register<RefreshInfoProvider, RoutedEventArgs>(nameof(RefreshCompleted), RoutingStrategies.Bubble);
public bool PeekingMode { get; internal set; }
public bool IsInteractingForRefresh
{
get => _isInteractingForRefresh;
internal set
{
var isInteractingForRefresh = value && !PeekingMode;
if (isInteractingForRefresh != _isInteractingForRefresh)
{
SetAndRaise(IsInteractingForRefreshProperty, ref _isInteractingForRefresh, isInteractingForRefresh);
}
}
}
public double InteractionRatio
{
get => _interactionRatio;
set
{
SetAndRaise(InteractionRatioProperty, ref _interactionRatio, value);
}
}
public double ExecutionRatio
{
get => DefaultExecutionRatio;
}
internal CompositionVisual? Visual => _visual;
public event EventHandler<RoutedEventArgs> RefreshStarted
{
add => AddHandler(RefreshStartedEvent, value);
remove => RemoveHandler(RefreshStartedEvent, value);
}
public event EventHandler<RoutedEventArgs> RefreshCompleted
{
add => AddHandler(RefreshCompletedEvent, value);
remove => RemoveHandler(RefreshCompletedEvent, value);
}
internal void InteractingStateEntered(object? sender, PullGestureEventArgs e)
{
if (!_entered)
{
IsInteractingForRefresh = true;
_entered = true;
}
ValuesChanged(e.Delta);
}
internal void InteractingStateExited(object? sender, PullGestureEndedEventArgs e)
{
IsInteractingForRefresh = false;
_entered = false;
ValuesChanged(default);
}
public RefreshInfoProvider(PullDirection refreshPullDirection, Size? refreshVIsualizerSize, CompositionVisual? visual)
{
_refreshPullDirection = refreshPullDirection;
_refreshVisualizerSize = refreshVIsualizerSize ?? default;
_visual = visual;
}
public void OnRefreshStarted()
{
RaiseEvent(new RoutedEventArgs(RefreshStartedEvent));
}
public void OnRefreshCompleted()
{
RaiseEvent(new RoutedEventArgs(RefreshCompletedEvent));
}
internal void ValuesChanged(Vector value)
{
switch (_refreshPullDirection)
{
case PullDirection.TopToBottom:
case PullDirection.BottomToTop:
InteractionRatio = _refreshVisualizerSize.Height == 0 ? 1 : Math.Min(1, value.Y / _refreshVisualizerSize.Height);
break;
case PullDirection.LeftToRight:
case PullDirection.RightToLeft:
InteractionRatio = _refreshVisualizerSize.Height == 0 ? 1 : Math.Min(1, value.X / _refreshVisualizerSize.Width);
break;
}
}
}
}

42
src/Avalonia.Controls/PullToRefresh/RefreshRequestedEventArgs.cs

@ -0,0 +1,42 @@
using System;
using Avalonia.Interactivity;
namespace Avalonia.Controls
{
/// <summary>
/// Provides event data for RefreshRequested events.
/// </summary>
public class RefreshRequestedEventArgs : RoutedEventArgs
{
private RefreshCompletionDeferral _refreshCompletionDeferral;
/// <summary>
/// Gets a deferral object for managing the work done in the RefreshRequested event handler.
/// </summary>
/// <returns>A <see cref="RefreshCompletionDeferral"/> object</returns>
public RefreshCompletionDeferral GetDeferral()
{
return _refreshCompletionDeferral.Get();
}
public RefreshRequestedEventArgs(Action deferredAction, RoutedEvent? routedEvent) : base(routedEvent)
{
_refreshCompletionDeferral = new RefreshCompletionDeferral(deferredAction);
}
public RefreshRequestedEventArgs(RefreshCompletionDeferral completionDeferral, RoutedEvent? routedEvent) : base(routedEvent)
{
_refreshCompletionDeferral = completionDeferral;
}
internal void IncrementCount()
{
_refreshCompletionDeferral?.Get();
}
internal void DecrementCount()
{
_refreshCompletionDeferral?.Complete();
}
}
}

553
src/Avalonia.Controls/PullToRefresh/RefreshVisualizer.cs

@ -0,0 +1,553 @@
using System;
using System.Numerics;
using System.Reactive.Linq;
using Avalonia.Animation.Easings;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.PullToRefresh;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Animations;
namespace Avalonia.Controls
{
public class RefreshVisualizer : ContentControl
{
private const int DefaultIndicatorSize = 24;
private const float MinimumIndicatorOpacity = 0.4f;
private const float ParallaxPositionRatio = 0.5f;
private double _executingRatio = 0.8;
private RefreshVisualizerState _refreshVisualizerState;
private RefreshInfoProvider? _refreshInfoProvider;
private IDisposable? _isInteractingSubscription;
private IDisposable? _interactionRatioSubscription;
private bool _isInteractingForRefresh;
private Grid? _root;
private Control? _content;
private RefreshVisualizerOrientation _orientation;
private float _startingRotationAngle;
private double _interactionRatio;
private bool _played;
private ScalarKeyFrameAnimation? _rotateAnimation;
private bool IsPullDirectionVertical => PullDirection == PullDirection.TopToBottom || PullDirection == PullDirection.BottomToTop;
private bool IsPullDirectionFar => PullDirection == PullDirection.BottomToTop || PullDirection == PullDirection.RightToLeft;
/// <summary>
/// Defines the <see cref="PullDirection"/> property.
/// </summary>
public static readonly StyledProperty<PullDirection> PullDirectionProperty =
AvaloniaProperty.Register<RefreshVisualizer, PullDirection>(nameof(PullDirection), PullDirection.TopToBottom);
/// <summary>
/// Defines the <see cref="RefreshRequested"/> event.
/// </summary>
public static readonly RoutedEvent<RefreshRequestedEventArgs> RefreshRequestedEvent =
RoutedEvent.Register<RefreshVisualizer, RefreshRequestedEventArgs>(nameof(RefreshRequested), RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="RefreshVisualizerState"/> property.
/// </summary>
public static readonly DirectProperty<RefreshVisualizer, RefreshVisualizerState> RefreshVisualizerStateProperty =
AvaloniaProperty.RegisterDirect<RefreshVisualizer, RefreshVisualizerState>(nameof(RefreshVisualizerState),
s => s.RefreshVisualizerState);
/// <summary>
/// Defines the <see cref="Orientation"/> property.
/// </summary>
public static readonly DirectProperty<RefreshVisualizer, RefreshVisualizerOrientation> OrientationProperty =
AvaloniaProperty.RegisterDirect<RefreshVisualizer, RefreshVisualizerOrientation>(nameof(Orientation),
s => s.Orientation, (s, o) => s.Orientation = o);
/// <summary>
/// Defines the <see cref="RefreshInfoProvider"/> property.
/// </summary>
internal DirectProperty<RefreshVisualizer, RefreshInfoProvider?> RefreshInfoProviderProperty =
AvaloniaProperty.RegisterDirect<RefreshVisualizer, RefreshInfoProvider?>(nameof(RefreshInfoProvider),
s => s.RefreshInfoProvider, (s, o) => s.RefreshInfoProvider = o);
/// <summary>
/// Gets or sets a value that indicates the refresh state of the visualizer.
/// </summary>
protected RefreshVisualizerState RefreshVisualizerState
{
get
{
return _refreshVisualizerState;
}
private set
{
SetAndRaise(RefreshVisualizerStateProperty, ref _refreshVisualizerState, value);
UpdateContent();
}
}
/// <summary>
/// Gets or sets a value that indicates the orientation of the visualizer.
/// </summary>
public RefreshVisualizerOrientation Orientation
{
get
{
return _orientation;
}
set
{
SetAndRaise(OrientationProperty, ref _orientation, value);
}
}
internal PullDirection PullDirection
{
get => GetValue(PullDirectionProperty);
set => SetValue(PullDirectionProperty, value);
}
internal RefreshInfoProvider? RefreshInfoProvider
{
get => _refreshInfoProvider;
set
{
if (_refreshInfoProvider != null)
{
_refreshInfoProvider.RenderTransform = null;
}
SetAndRaise(RefreshInfoProviderProperty, ref _refreshInfoProvider, value);
}
}
/// <summary>
/// Occurs when an update of the content has been initiated.
/// </summary>
public event EventHandler<RefreshRequestedEventArgs>? RefreshRequested
{
add => AddHandler(RefreshRequestedEvent, value);
remove => RemoveHandler(RefreshRequestedEvent, value);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
this.ClipToBounds = false;
_root = e.NameScope.Find<Grid>("PART_Root");
if (_root != null)
{
_content = Content as Control;
if (_content == null)
{
_content = new PathIcon()
{
Height = DefaultIndicatorSize,
Width = DefaultIndicatorSize,
Name = "PART_Icon"
};
_content.Loaded += (s, e) =>
{
var composition = ElementComposition.GetElementVisual(_content);
var compositor = composition!.Compositor;
composition.Opacity = 0;
var smoothRotationAnimation
= compositor.CreateScalarKeyFrameAnimation();
smoothRotationAnimation.Target = "RotationAngle";
smoothRotationAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue", new LinearEasing());
smoothRotationAnimation.Duration = TimeSpan.FromMilliseconds(100);
var opacityAnimation
= compositor.CreateScalarKeyFrameAnimation();
opacityAnimation.Target = "Opacity";
opacityAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue", new LinearEasing());
opacityAnimation.Duration = TimeSpan.FromMilliseconds(100);
var offsetAnimation = compositor.CreateVector3KeyFrameAnimation();
offsetAnimation.Target = "Offset";
offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue", new LinearEasing());
offsetAnimation.Duration = TimeSpan.FromMilliseconds(150);
var scaleAnimation
= compositor.CreateVector3KeyFrameAnimation();
scaleAnimation.Target = "Scale";
scaleAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue", new LinearEasing());
scaleAnimation.Duration = TimeSpan.FromMilliseconds(100);
var animation = compositor.CreateImplicitAnimationCollection();
animation["RotationAngle"] = smoothRotationAnimation;
animation["Offset"] = offsetAnimation;
animation["Scale"] = scaleAnimation;
animation["Opacity"] = opacityAnimation;
composition.ImplicitAnimations = animation;
UpdateContent();
};
Content = _content;
}
else
{
RaisePropertyChanged(ContentProperty, null, Content, Data.BindingPriority.Style, false);
}
}
OnOrientationChanged();
UpdateContent();
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
UpdateContent();
}
private void UpdateContent()
{
if (_content != null && _root != null)
{
var root = _root;
var visual = _refreshInfoProvider?.Visual;
var contentVisual = ElementComposition.GetElementVisual(_content);
var visualizerVisual = ElementComposition.GetElementVisual(this);
if (visual != null && contentVisual != null && visualizerVisual != null)
{
contentVisual.CenterPoint = new Vector3((float)(_content.Bounds.Width / 2), (float)(_content.Bounds.Height / 2), 0);
switch (RefreshVisualizerState)
{
case RefreshVisualizerState.Idle:
_played = false;
if(_rotateAnimation != null)
{
_rotateAnimation.IterationBehavior = AnimationIterationBehavior.Count;
_rotateAnimation = null;
}
contentVisual.Opacity = MinimumIndicatorOpacity;
contentVisual.RotationAngle = _startingRotationAngle;
visualizerVisual.Offset = IsPullDirectionVertical ?
new Vector3(visualizerVisual.Offset.X, 0, 0) :
new Vector3(0, visualizerVisual.Offset.Y, 0);
visual.Offset = default;
_content.InvalidateMeasure();
break;
case RefreshVisualizerState.Interacting:
_played = false;
contentVisual.Opacity = MinimumIndicatorOpacity;
contentVisual.RotationAngle = (float)(_startingRotationAngle + _interactionRatio * 2 * Math.PI);
Vector3 offset = default;
if (IsPullDirectionVertical)
{
offset = new Vector3(0, (float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0);
}
else
{
offset = new Vector3((float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0);
}
visual.Offset = offset;
visualizerVisual.Offset = IsPullDirectionVertical ?
new Vector3(visualizerVisual.Offset.X, offset.Y, 0) :
new Vector3(offset.X, visualizerVisual.Offset.Y, 0);
break;
case RefreshVisualizerState.Pending:
contentVisual.Opacity = 1;
contentVisual.RotationAngle = _startingRotationAngle + (float)(2 * Math.PI);
if (IsPullDirectionVertical)
{
offset = new Vector3(0, (float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0);
}
else
{
offset = new Vector3((float)(_interactionRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0);
}
visual.Offset = offset;
visualizerVisual.Offset = IsPullDirectionVertical ?
new Vector3(visualizerVisual.Offset.X, offset.Y, 0) :
new Vector3(offset.X, visualizerVisual.Offset.Y, 0);
if (!_played)
{
_played = true;
var scaleAnimation = contentVisual.Compositor!.CreateVector3KeyFrameAnimation();
scaleAnimation.Target = "Scale";
scaleAnimation.InsertKeyFrame(0.5f, new Vector3(1.5f, 1.5f, 1));
scaleAnimation.InsertKeyFrame(1f, new Vector3(1f, 1f, 1));
scaleAnimation.Duration = TimeSpan.FromSeconds(0.3);
contentVisual.StartAnimation("Scale", scaleAnimation);
}
break;
case RefreshVisualizerState.Refreshing:
_rotateAnimation = contentVisual.Compositor!.CreateScalarKeyFrameAnimation();
_rotateAnimation.Target = "RotationAngle";
_rotateAnimation.InsertKeyFrame(0, _startingRotationAngle, new LinearEasing());
_rotateAnimation.InsertKeyFrame(1, _startingRotationAngle + (float)(2 * Math.PI), new LinearEasing());
_rotateAnimation.IterationBehavior = AnimationIterationBehavior.Forever;
_rotateAnimation.StopBehavior = AnimationStopBehavior.LeaveCurrentValue;
_rotateAnimation.Duration = TimeSpan.FromSeconds(0.5);
contentVisual.StartAnimation("RotationAngle", _rotateAnimation);
contentVisual.Opacity = 1;
float translationRatio = (float)(_refreshInfoProvider != null ? (1.0f - _refreshInfoProvider.ExecutionRatio) * ParallaxPositionRatio : 1.0f)
* (IsPullDirectionFar ? -1f : 1f);
if (IsPullDirectionVertical)
{
offset = new Vector3(0, (float)(_executingRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Height), 0);
}
else
{
offset = new Vector3((float)(_executingRatio * (IsPullDirectionFar ? -1 : 1) * root.Bounds.Width), 0, 0);
}
visual.Offset = offset;
contentVisual.Offset += IsPullDirectionVertical ? new Vector3(0, (float)(translationRatio * root.Bounds.Height), 0) :
new Vector3((float)(translationRatio * root.Bounds.Width), 0, 0);
visualizerVisual.Offset = IsPullDirectionVertical ?
new Vector3(visualizerVisual.Offset.X, offset.Y, 0) :
new Vector3(offset.X, visualizerVisual.Offset.Y, 0);
break;
case RefreshVisualizerState.Peeking:
contentVisual.Opacity = 1;
contentVisual.RotationAngle = _startingRotationAngle;
break;
}
}
}
}
/// <summary>
/// Initiates an update of the content.
/// </summary>
public void RequestRefresh()
{
RefreshVisualizerState = RefreshVisualizerState.Refreshing;
RefreshInfoProvider?.OnRefreshStarted();
RaiseRefreshRequested();
}
private void RefreshCompleted()
{
RefreshVisualizerState = RefreshVisualizerState.Idle;
RefreshInfoProvider?.OnRefreshCompleted();
}
private void RaiseRefreshRequested()
{
var refreshArgs = new RefreshRequestedEventArgs(RefreshCompleted, RefreshRequestedEvent);
refreshArgs.IncrementCount();
RaiseEvent(refreshArgs);
refreshArgs.DecrementCount();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == RefreshInfoProviderProperty)
{
OnRefreshInfoProviderChanged();
}
else if (change.Property == ContentProperty)
{
if (_root != null && _content != null)
{
_root.Children.Insert(0, _content);
_content.VerticalAlignment = Layout.VerticalAlignment.Center;
_content.HorizontalAlignment = Layout.HorizontalAlignment.Center;
}
UpdateContent();
}
else if (change.Property == OrientationProperty)
{
OnOrientationChanged();
UpdateContent();
}
else if (change.Property == BoundsProperty)
{
switch (PullDirection)
{
case PullDirection.TopToBottom:
RenderTransform = new TranslateTransform(0, -Bounds.Height);
break;
case PullDirection.BottomToTop:
RenderTransform = new TranslateTransform(0, Bounds.Height);
break;
case PullDirection.LeftToRight:
RenderTransform = new TranslateTransform(-Bounds.Width, 0);
break;
case PullDirection.RightToLeft:
RenderTransform = new TranslateTransform(Bounds.Width, 0);
break;
}
UpdateContent();
}
else if(change.Property == PullDirectionProperty)
{
OnOrientationChanged();
UpdateContent();
}
}
private void OnOrientationChanged()
{
switch (_orientation)
{
case RefreshVisualizerOrientation.Auto:
switch (PullDirection)
{
case PullDirection.TopToBottom:
case PullDirection.BottomToTop:
_startingRotationAngle = 0.0f;
break;
case PullDirection.LeftToRight:
_startingRotationAngle = (float)(-Math.PI / 2);
break;
case PullDirection.RightToLeft:
_startingRotationAngle = (float)(Math.PI / 2);
break;
}
break;
case RefreshVisualizerOrientation.Normal:
_startingRotationAngle = 0.0f;
break;
case RefreshVisualizerOrientation.Rotate90DegreesCounterclockwise:
_startingRotationAngle = (float)(Math.PI / 2);
break;
case RefreshVisualizerOrientation.Rotate270DegreesCounterclockwise:
_startingRotationAngle = (float)(-Math.PI / 2);
break;
}
}
private void OnRefreshInfoProviderChanged()
{
_isInteractingSubscription?.Dispose();
_isInteractingSubscription = null;
_interactionRatioSubscription?.Dispose();
_interactionRatioSubscription = null;
if (RefreshInfoProvider != null)
{
_isInteractingSubscription = RefreshInfoProvider.GetObservable(RefreshInfoProvider.IsInteractingForRefreshProperty)
.Subscribe(InteractingForRefreshObserver);
_interactionRatioSubscription = RefreshInfoProvider.GetObservable(RefreshInfoProvider.InteractionRatioProperty)
.Subscribe(InteractionRatioObserver);
var visual = RefreshInfoProvider.Visual;
_executingRatio = RefreshInfoProvider.ExecutionRatio;
}
else
{
_executingRatio = 1;
}
}
private void InteractionRatioObserver(double obj)
{
var wasAtZero = _interactionRatio == 0.0;
_interactionRatio = obj;
if (_isInteractingForRefresh)
{
if (RefreshVisualizerState == RefreshVisualizerState.Idle)
{
if (wasAtZero)
{
if (_interactionRatio > _executingRatio)
{
RefreshVisualizerState = RefreshVisualizerState.Pending;
}
else if (_interactionRatio > 0)
{
RefreshVisualizerState = RefreshVisualizerState.Interacting;
}
}
else if (_interactionRatio > 0)
{
RefreshVisualizerState = RefreshVisualizerState.Peeking;
}
}
else if (RefreshVisualizerState == RefreshVisualizerState.Interacting)
{
if (_interactionRatio <= 0)
{
RefreshVisualizerState = RefreshVisualizerState.Idle;
}
else if (_interactionRatio > _executingRatio)
{
RefreshVisualizerState = RefreshVisualizerState.Pending;
}
else
{
UpdateContent();
}
}
else if (RefreshVisualizerState == RefreshVisualizerState.Pending)
{
if (_interactionRatio <= _executingRatio)
{
RefreshVisualizerState = RefreshVisualizerState.Interacting;
}
else if (_interactionRatio <= 0)
{
RefreshVisualizerState = RefreshVisualizerState.Idle;
}
else
{
UpdateContent();
}
}
}
else
{
if (RefreshVisualizerState != RefreshVisualizerState.Refreshing)
{
if (_interactionRatio > 0)
{
RefreshVisualizerState = RefreshVisualizerState.Peeking;
}
else
{
RefreshVisualizerState = RefreshVisualizerState.Idle;
}
}
}
}
private void InteractingForRefreshObserver(bool obj)
{
_isInteractingForRefresh = obj;
if (!_isInteractingForRefresh)
{
switch (_refreshVisualizerState)
{
case RefreshVisualizerState.Pending:
RequestRefresh();
break;
case RefreshVisualizerState.Refreshing:
// We don't want to interrupt a currently executing refresh.
break;
default:
RefreshVisualizerState = RefreshVisualizerState.Idle;
break;
}
}
}
}
}

13
src/Avalonia.Controls/PullToRefresh/RefreshVisualizerOrientation.cs

@ -0,0 +1,13 @@
namespace Avalonia.Controls
{
/// <summary>
/// Defines constants that specify the orientation of a RefreshVisualizer.
/// </summary>
public enum RefreshVisualizerOrientation
{
Auto,
Normal,
Rotate90DegreesCounterclockwise,
Rotate270DegreesCounterclockwise
}
}

14
src/Avalonia.Controls/PullToRefresh/RefreshVisualizerState.cs

@ -0,0 +1,14 @@
namespace Avalonia.Controls
{
/// <summary>
/// Defines constants that specify the state of a RefreshVisualizer
/// </summary>
public enum RefreshVisualizerState
{
Idle,
Peeking,
Interacting,
Pending,
Refreshing
}
}

274
src/Avalonia.Controls/PullToRefresh/ScrollViewerIRefreshInfoProviderAdapter.cs

@ -0,0 +1,274 @@
using System;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Rendering.Composition;
using Avalonia.VisualTree;
namespace Avalonia.Controls.PullToRefresh
{
internal class ScrollViewerIRefreshInfoProviderAdapter
{
private const int MaxSearchDepth = 10;
private const int InitialOffsetThreshold = 1;
private PullDirection _refreshPullDirection;
private ScrollViewer? _scrollViewer;
private RefreshInfoProvider? _refreshInfoProvider;
private PullGestureRecognizer? _pullGestureRecognizer;
private InputElement? _interactionSource;
private bool _isVisualizerInteractionSourceAttached;
public ScrollViewerIRefreshInfoProviderAdapter(PullDirection pullDirection)
{
_refreshPullDirection = pullDirection;
}
public RefreshInfoProvider? AdaptFromTree(Visual root, Size? refreshVIsualizerSize)
{
if (root is ScrollViewer scrollViewer)
{
return Adapt(scrollViewer, refreshVIsualizerSize);
}
else
{
int depth = 0;
while (depth < MaxSearchDepth)
{
var scroll = AdaptFromTreeRecursiveHelper(root, depth);
if (scroll != null)
{
return Adapt(scroll, refreshVIsualizerSize);
}
depth++;
}
}
ScrollViewer? AdaptFromTreeRecursiveHelper(Visual root, int depth)
{
if (depth == 0)
{
foreach (var child in root.VisualChildren)
{
if (child is ScrollViewer viewer)
{
return viewer;
}
}
}
else
{
foreach (var child in root.VisualChildren)
{
var viewer = AdaptFromTreeRecursiveHelper(child, depth - 1);
if (viewer != null)
{
return viewer;
}
}
}
return null;
}
return null;
}
public RefreshInfoProvider Adapt(ScrollViewer adaptee, Size? refreshVIsualizerSize)
{
if (adaptee == null)
{
throw new ArgumentNullException(nameof(adaptee), "Adaptee cannot be null");
}
if (_scrollViewer != null)
{
CleanUpScrollViewer();
}
if (_refreshInfoProvider != null && _interactionSource != null)
{
_interactionSource.RemoveHandler(Gestures.PullGestureEvent, _refreshInfoProvider.InteractingStateEntered);
_interactionSource.RemoveHandler(Gestures.PullGestureEndedEvent, _refreshInfoProvider.InteractingStateExited);
}
_refreshInfoProvider = null;
_scrollViewer = adaptee;
if (_scrollViewer.Content == null)
{
throw new ArgumentException(nameof(adaptee), "Adaptee's content property cannot be null.");
}
var content = adaptee.Content as Visual;
if (content == null)
{
throw new ArgumentException(nameof(adaptee), "Adaptee's content property must be a Visual");
}
if (content.GetVisualParent() == null)
{
_scrollViewer.Loaded += ScrollViewer_Loaded;
}
else
{
ScrollViewer_Loaded(null, null);
if (content.Parent is not InputElement)
{
throw new ArgumentException(nameof(adaptee), "Adaptee's content's parent must be a InputElement");
}
}
_refreshInfoProvider = new RefreshInfoProvider(_refreshPullDirection, refreshVIsualizerSize, ElementComposition.GetElementVisual(content));
_pullGestureRecognizer = new PullGestureRecognizer(_refreshPullDirection);
if (_interactionSource != null)
{
_interactionSource.GestureRecognizers.Add(_pullGestureRecognizer);
_interactionSource.AddHandler(Gestures.PullGestureEvent, _refreshInfoProvider.InteractingStateEntered);
_interactionSource.AddHandler(Gestures.PullGestureEndedEvent, _refreshInfoProvider.InteractingStateExited);
_isVisualizerInteractionSourceAttached = true;
}
_scrollViewer.PointerPressed += ScrollViewer_PointerPressed;
_scrollViewer.PointerReleased += ScrollViewer_PointerReleased;
_scrollViewer.ScrollChanged += ScrollViewer_ScrollChanged;
return _refreshInfoProvider;
}
private void ScrollViewer_ScrollChanged(object? sender, ScrollChangedEventArgs e)
{
if (_isVisualizerInteractionSourceAttached && _refreshInfoProvider != null && _refreshInfoProvider.IsInteractingForRefresh)
{
if (!IsWithinOffsetThreashold())
{
_refreshInfoProvider.IsInteractingForRefresh = false;
}
}
}
public void SetAnimations(RefreshVisualizer refreshVisualizer)
{
var visualizerComposition = ElementComposition.GetElementVisual(refreshVisualizer);
if (visualizerComposition != null)
{
var compositor = visualizerComposition.Compositor;
var offsetAnimation = compositor.CreateVector3KeyFrameAnimation();
offsetAnimation.Target = "Offset";
offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue");
offsetAnimation.Duration = TimeSpan.FromMilliseconds(150);
var animation = compositor.CreateImplicitAnimationCollection();
animation["Offset"] = offsetAnimation;
visualizerComposition.ImplicitAnimations = animation;
}
if(_scrollViewer != null && _scrollViewer.Content is Visual visual)
{
var scollContentComposition = ElementComposition.GetElementVisual(visual);
if(scollContentComposition != null)
{
var compositor = scollContentComposition.Compositor;
var offsetAnimation = compositor.CreateVector3KeyFrameAnimation();
offsetAnimation.Target = "Offset";
offsetAnimation.InsertExpressionKeyFrame(1.0f, "this.FinalValue");
offsetAnimation.Duration = TimeSpan.FromMilliseconds(150);
var animation = compositor.CreateImplicitAnimationCollection();
animation["Offset"] = offsetAnimation;
scollContentComposition.ImplicitAnimations = animation;
}
}
}
private void ScrollViewer_Loaded(object? sender, RoutedEventArgs? e)
{
var content = _scrollViewer?.Content as Visual;
if (content == null)
{
throw new ArgumentException(nameof(_scrollViewer), "Adaptee's content property must be a Visual");
}
if (content.Parent is not InputElement)
{
throw new ArgumentException(nameof(_scrollViewer), "Adaptee's content parent must be an InputElement");
}
MakeInteractionSource(content.Parent as InputElement);
if (_scrollViewer != null)
{
_scrollViewer.Loaded -= ScrollViewer_Loaded;
}
}
private void MakeInteractionSource(InputElement? element)
{
_interactionSource = element;
if (_pullGestureRecognizer != null && _refreshInfoProvider != null)
{
element?.GestureRecognizers.Add(_pullGestureRecognizer);
_interactionSource?.AddHandler(Gestures.PullGestureEvent, _refreshInfoProvider.InteractingStateEntered);
_interactionSource?.AddHandler(Gestures.PullGestureEndedEvent, _refreshInfoProvider.InteractingStateExited);
_isVisualizerInteractionSourceAttached = true;
}
}
private void ScrollViewer_PointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (_refreshInfoProvider != null)
{
_refreshInfoProvider.IsInteractingForRefresh = false;
}
}
private void ScrollViewer_PointerPressed(object? sender, PointerPressedEventArgs e)
{
if (_refreshInfoProvider != null)
{
_refreshInfoProvider.PeekingMode = !IsWithinOffsetThreashold();
}
}
private bool IsWithinOffsetThreashold()
{
if (_scrollViewer != null)
{
var offset = _scrollViewer.Offset;
switch (_refreshPullDirection)
{
case PullDirection.TopToBottom:
return offset.Y < InitialOffsetThreshold;
case PullDirection.LeftToRight:
return offset.X < InitialOffsetThreshold;
case PullDirection.RightToLeft:
return offset.X > _scrollViewer.Extent.Width - _scrollViewer.Viewport.Width - InitialOffsetThreshold;
case PullDirection.BottomToTop:
return offset.Y > _scrollViewer.Extent.Height - _scrollViewer.Viewport.Height - InitialOffsetThreshold;
}
}
return false;
}
private void CleanUpScrollViewer()
{
if (_scrollViewer != null)
{
_scrollViewer.PointerPressed -= ScrollViewer_PointerPressed;
_scrollViewer.PointerReleased -= ScrollViewer_PointerReleased;
_scrollViewer.ScrollChanged -= ScrollViewer_ScrollChanged;
}
}
}
}

6
src/Avalonia.Controls/TopLevel.cs

@ -357,12 +357,6 @@ namespace Avalonia.Controls
/// </summary>
protected virtual ILayoutManager CreateLayoutManager() => new LayoutManager(this);
public override void InvalidateMirrorTransform()
{
}
protected override bool BypassFlowDirectionPolicies => true;
/// <summary>
/// Handles a paint notification from <see cref="ITopLevelImpl.Resized"/>.
/// </summary>

267
src/Avalonia.Controls/Window.cs

@ -176,6 +176,8 @@ namespace Avalonia.Controls
private object? _dialogResult;
private readonly Size _maxPlatformClientSize;
private WindowStartupLocation _windowStartupLocation;
private bool _shown;
private bool _showingAsDialog;
/// <summary>
/// Initializes static members of the <see cref="Window"/> class.
@ -508,6 +510,8 @@ namespace Avalonia.Controls
Owner = null;
PlatformImpl?.Dispose();
_showingAsDialog = false;
}
private bool ShouldCancelClose(CancelEventArgs? args = null)
@ -563,29 +567,33 @@ namespace Avalonia.Controls
/// </summary>
public override void Hide()
{
if (!IsVisible)
using (FreezeVisibilityChangeHandling())
{
return;
}
if (!_shown)
{
return;
}
Renderer?.Stop();
Renderer?.Stop();
if (Owner is Window owner)
{
owner.RemoveChild(this);
}
if (Owner is Window owner)
{
owner.RemoveChild(this);
}
if (_children.Count > 0)
{
foreach (var child in _children.ToArray())
if (_children.Count > 0)
{
child.child.Hide();
foreach (var child in _children.ToArray())
{
child.child.Hide();
}
}
}
Owner = null;
PlatformImpl?.Hide();
IsVisible = false;
Owner = null;
PlatformImpl?.Hide();
IsVisible = false;
_shown = false;
}
}
/// <summary>
@ -599,81 +607,124 @@ namespace Avalonia.Controls
ShowCore(null);
}
protected override void IsVisibleChanged(AvaloniaPropertyChangedEventArgs e)
{
if (!IgnoreVisibilityChanges)
{
var isVisible = e.GetNewValue<bool>();
if (_shown != isVisible)
{
if(!_shown)
{
Show();
}
else
{
if (_showingAsDialog)
{
Close(false);
}
else
{
Hide();
}
}
}
}
}
/// <summary>
/// Shows the window as a child of <paramref name="parent"/>.
/// Shows the window as a child of <paramref name="owner"/>.
/// </summary>
/// <param name="parent">Window that will be a parent of the shown window.</param>
/// <param name="owner">Window that will be the owner of the shown window.</param>
/// <exception cref="InvalidOperationException">
/// The window has already been closed.
/// </exception>
public void Show(Window parent)
public void Show(Window owner)
{
if (parent is null)
if (owner is null)
{
throw new ArgumentNullException(nameof(parent), "Showing a child window requires valid parent.");
throw new ArgumentNullException(nameof(owner), "Showing a child window requires valid parent.");
}
ShowCore(parent);
ShowCore(owner);
}
private void ShowCore(Window? parent)
private void EnsureStateBeforeShow()
{
if (PlatformImpl == null)
{
throw new InvalidOperationException("Cannot re-show a closed window.");
}
}
private void EnsureParentStateBeforeShow(Window owner)
{
if (owner.PlatformImpl == null)
{
throw new InvalidOperationException("Cannot show a window with a closed owner.");
}
if (parent != null)
if (owner == this)
{
if (parent.PlatformImpl == null)
{
throw new InvalidOperationException("Cannot show a window with a closed parent.");
}
else if (parent == this)
{
throw new InvalidOperationException("A Window cannot be its own parent.");
}
else if (!parent.IsVisible)
{
throw new InvalidOperationException("Cannot show window with non-visible parent.");
}
throw new InvalidOperationException("A Window cannot be its own owner.");
}
if (IsVisible)
if (!owner.IsVisible)
{
return;
throw new InvalidOperationException("Cannot show window with non-visible owner.");
}
}
RaiseEvent(new RoutedEventArgs(WindowOpenedEvent));
private void ShowCore(Window? owner)
{
using (FreezeVisibilityChangeHandling())
{
EnsureStateBeforeShow();
EnsureInitialized();
ApplyStyling();
IsVisible = true;
if (owner != null)
{
EnsureParentStateBeforeShow(owner);
}
var initialSize = new Size(
double.IsNaN(Width) ? Math.Max(MinWidth, ClientSize.Width) : Width,
double.IsNaN(Height) ? Math.Max(MinHeight, ClientSize.Height) : Height);
if (_shown)
{
return;
}
if (initialSize != ClientSize)
{
PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout);
}
RaiseEvent(new RoutedEventArgs(WindowOpenedEvent));
LayoutManager.ExecuteInitialLayoutPass();
EnsureInitialized();
ApplyStyling();
_shown = true;
IsVisible = true;
if (PlatformImpl != null && parent?.PlatformImpl is not null)
{
PlatformImpl.SetParent(parent.PlatformImpl);
}
var initialSize = new Size(
double.IsNaN(Width) ? Math.Max(MinWidth, ClientSize.Width) : Width,
double.IsNaN(Height) ? Math.Max(MinHeight, ClientSize.Height) : Height);
Owner = parent;
parent?.AddChild(this, false);
if (initialSize != ClientSize)
{
PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout);
}
SetWindowStartupLocation(parent?.PlatformImpl);
LayoutManager.ExecuteInitialLayoutPass();
PlatformImpl?.Show(ShowActivated, false);
Renderer?.Start();
OnOpened(EventArgs.Empty);
if (PlatformImpl != null && owner?.PlatformImpl is not null)
{
PlatformImpl.SetParent(owner.PlatformImpl);
}
Owner = owner;
owner?.AddChild(this, false);
SetWindowStartupLocation(owner?.PlatformImpl);
PlatformImpl?.Show(ShowActivated, false);
Renderer?.Start();
OnOpened(EventArgs.Empty);
}
}
/// <summary>
@ -703,68 +754,66 @@ namespace Avalonia.Controls
/// </returns>
public Task<TResult> ShowDialog<TResult>(Window owner)
{
if (owner == null)
{
throw new ArgumentNullException(nameof(owner));
}
else if (owner.PlatformImpl == null)
{
throw new InvalidOperationException("Cannot show a window with a closed owner.");
}
else if (owner == this)
using (FreezeVisibilityChangeHandling())
{
throw new InvalidOperationException("A Window cannot be its own owner.");
}
else if (IsVisible)
{
throw new InvalidOperationException("The window is already being shown.");
}
else if (!owner.IsVisible)
{
throw new InvalidOperationException("Cannot show window with non-visible parent.");
}
EnsureStateBeforeShow();
RaiseEvent(new RoutedEventArgs(WindowOpenedEvent));
if (owner == null)
{
throw new ArgumentNullException(nameof(owner));
}
EnsureInitialized();
ApplyStyling();
IsVisible = true;
EnsureParentStateBeforeShow(owner);
var initialSize = new Size(
double.IsNaN(Width) ? ClientSize.Width : Width,
double.IsNaN(Height) ? ClientSize.Height : Height);
if (_shown)
{
throw new InvalidOperationException("The window is already being shown.");
}
if (initialSize != ClientSize)
{
PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout);
}
RaiseEvent(new RoutedEventArgs(WindowOpenedEvent));
LayoutManager.ExecuteInitialLayoutPass();
EnsureInitialized();
ApplyStyling();
_shown = true;
_showingAsDialog = true;
IsVisible = true;
var result = new TaskCompletionSource<TResult>();
var initialSize = new Size(
double.IsNaN(Width) ? ClientSize.Width : Width,
double.IsNaN(Height) ? ClientSize.Height : Height);
PlatformImpl?.SetParent(owner.PlatformImpl);
Owner = owner;
owner.AddChild(this, true);
if (initialSize != ClientSize)
{
PlatformImpl?.Resize(initialSize, PlatformResizeReason.Layout);
}
SetWindowStartupLocation(owner.PlatformImpl);
LayoutManager.ExecuteInitialLayoutPass();
PlatformImpl?.Show(ShowActivated, true);
var result = new TaskCompletionSource<TResult>();
Renderer?.Start();
PlatformImpl?.SetParent(owner.PlatformImpl!);
Owner = owner;
owner.AddChild(this, true);
Observable.FromEventPattern<EventHandler, EventArgs>(
x => Closed += x,
x => Closed -= x)
.Take(1)
.Subscribe(_ =>
{
owner.Activate();
result.SetResult((TResult)(_dialogResult ?? default(TResult)!));
});
SetWindowStartupLocation(owner.PlatformImpl);
OnOpened(EventArgs.Empty);
return result.Task;
PlatformImpl?.Show(ShowActivated, true);
Renderer?.Start();
Observable.FromEventPattern<EventHandler, EventArgs>(
x => Closed += x,
x => Closed -= x)
.Take(1)
.Subscribe(_ =>
{
owner.Activate();
result.SetResult((TResult)(_dialogResult ?? default(TResult)!));
});
OnOpened(EventArgs.Empty);
return result.Task;
}
}
private void UpdateEnabled()

58
src/Avalonia.Controls/WindowBase.cs

@ -42,9 +42,11 @@ namespace Avalonia.Controls
private bool _hasExecutedInitialLayoutPass;
private bool _isActive;
private bool _ignoreVisibilityChange;
private int _ignoreVisibilityChanges;
private WindowBase? _owner;
protected bool IgnoreVisibilityChanges => _ignoreVisibilityChanges > 0;
static WindowBase()
{
IsVisibleProperty.OverrideDefaultValue<WindowBase>(false);
@ -66,6 +68,11 @@ namespace Avalonia.Controls
impl.PositionChanged = HandlePositionChanged;
}
protected IDisposable FreezeVisibilityChangeHandling()
{
return new IgnoreVisibilityChangesDisposable(this);
}
/// <summary>
/// Fired when the window is activated.
/// </summary>
@ -125,18 +132,12 @@ namespace Avalonia.Controls
/// </summary>
public virtual void Hide()
{
_ignoreVisibilityChange = true;
try
using (FreezeVisibilityChangeHandling())
{
Renderer?.Stop();
PlatformImpl?.Hide();
IsVisible = false;
}
finally
{
_ignoreVisibilityChange = false;
}
}
/// <summary>
@ -144,9 +145,7 @@ namespace Avalonia.Controls
/// </summary>
public virtual void Show()
{
_ignoreVisibilityChange = true;
try
using (FreezeVisibilityChangeHandling())
{
EnsureInitialized();
ApplyStyling();
@ -157,14 +156,11 @@ namespace Avalonia.Controls
LayoutManager.ExecuteInitialLayoutPass();
_hasExecutedInitialLayoutPass = true;
}
PlatformImpl?.Show(true, false);
Renderer?.Start();
OnOpened(EventArgs.Empty);
}
finally
{
_ignoreVisibilityChange = false;
}
}
/// <summary>
@ -202,23 +198,17 @@ namespace Avalonia.Controls
protected override void HandleClosed()
{
_ignoreVisibilityChange = true;
try
using (FreezeVisibilityChangeHandling())
{
IsVisible = false;
if (this is IFocusScope scope)
{
FocusManager.Instance?.RemoveFocusScope(scope);
}
base.HandleClosed();
}
finally
{
_ignoreVisibilityChange = false;
}
}
/// <summary>
@ -318,9 +308,9 @@ namespace Avalonia.Controls
Deactivated?.Invoke(this, EventArgs.Empty);
}
private void IsVisibleChanged(AvaloniaPropertyChangedEventArgs e)
protected virtual void IsVisibleChanged(AvaloniaPropertyChangedEventArgs e)
{
if (!_ignoreVisibilityChange)
if (_ignoreVisibilityChanges == 0)
{
if ((bool)e.NewValue!)
{
@ -332,5 +322,21 @@ namespace Avalonia.Controls
}
}
}
private readonly struct IgnoreVisibilityChangesDisposable : IDisposable
{
private readonly WindowBase _windowBase;
public IgnoreVisibilityChangesDisposable(WindowBase windowBase)
{
_windowBase = windowBase;
_windowBase._ignoreVisibilityChanges++;
}
public void Dispose()
{
_windowBase._ignoreVisibilityChanges--;
}
}
}
}

4
src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml

@ -636,4 +636,8 @@
<!-- BaseResources for ScrollViewer.xaml -->
<SolidColorBrush x:Key="ScrollViewerScrollBarsSeparatorBackground" Color="{StaticResource SystemChromeMediumColor}" Opacity="0.9" />
<!-- BaseResources for RefreshVisualizer.xaml -->
<SolidColorBrush x:Key="RefreshVisualizerForeground" Color="White"/>
<SolidColorBrush x:Key="RefreshVisualizerBackground" Color="Transparent"/>
</ResourceDictionary>

6
src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml

@ -631,4 +631,8 @@
<!-- Resources for ScrollViewer.xaml -->
<SolidColorBrush x:Key="ScrollViewerScrollBarsSeparatorBackground" Color="{StaticResource SystemChromeMediumColor}" Opacity="0.9" />
</ResourceDictionary>
<!-- BaseResources for RefreshVisualizer.xaml -->
<SolidColorBrush x:Key="RefreshVisualizerForeground" Color="Black"/>
<SolidColorBrush x:Key="RefreshVisualizerBackground" Color="Transparent"/>
</ResourceDictionary>

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

@ -68,6 +68,8 @@
<!-- ManagedFileChooser comes last because it uses (and overrides) styles for a multitude of other controls...the dialogs were originally UserControls, after all -->
<ResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/ManagedFileChooser.xaml" />
<ResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/SelectableTextBlock.xaml"/>
<ResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/RefreshContainer.xaml" />
<ResourceInclude Source="avares://Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Styles.Resources>

24
src/Avalonia.Themes.Fluent/Controls/RefreshContainer.xaml

@ -0,0 +1,24 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ControlTheme x:Key="{x:Type RefreshContainer}"
TargetType="RefreshContainer">
<Setter Property="Template">
<ControlTemplate>
<Grid>
<ContentPresenter Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}">
</ContentPresenter>
<Grid Name="PART_RefreshVisualizerPresenter"/>
</Grid>
</ControlTemplate>
</Setter>
</ControlTheme>
</ResourceDictionary>

29
src/Avalonia.Themes.Fluent/Controls/RefreshVisualizer.xaml

@ -0,0 +1,29 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ControlTheme x:Key="{x:Type RefreshVisualizer}"
TargetType="RefreshVisualizer">
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="IsHitTestVisible" Value="False"/>
<Setter Property="Height"
Value="100"/>
<Setter Property="Background"
Value="{DynamicResource RefreshVisualizerBackground}"/>
<Setter Property="Foreground"
Value="{DynamicResource RefreshVisualizerForeground}"/>
<Setter Property="Template">
<ControlTemplate>
<Grid Name="PART_Root"
MinHeight="80"
Background="{TemplateBinding Background}">
<Grid.Styles>
<Style Selector="PathIcon#PART_Icon">
<Setter Property="Data"
Value="M18.6195264,3.31842271 C19.0080059,3.31842271 19.3290603,3.60710385 19.3798716,3.9816481 L19.3868766,4.08577298 L19.3868766,6.97963208 C19.3868766,7.36811161 19.0981955,7.68916605 18.7236513,7.73997735 L18.6195264,7.74698235 L15.7256673,7.74698235 C15.3018714,7.74698235 14.958317,7.40342793 14.958317,6.97963208 C14.958317,6.59115255 15.2469981,6.27009811 15.6215424,6.21928681 L15.7256673,6.21228181 L16.7044011,6.21182461 C13.7917384,3.87107476 9.52212532,4.05209336 6.81933829,6.75488039 C3.92253872,9.65167996 3.92253872,14.34832 6.81933829,17.2451196 C9.71613786,20.1419192 14.4127779,20.1419192 17.3095775,17.2451196 C19.0725398,15.4821573 19.8106555,12.9925923 19.3476248,10.58925 C19.2674502,10.173107 19.5398064,9.77076216 19.9559494,9.69058758 C20.3720923,9.610413 20.7744372,9.88276918 20.8546118,10.2989121 C21.4129973,13.1971899 20.5217103,16.2033812 18.3947747,18.3303168 C14.8986373,21.8264542 9.23027854,21.8264542 5.73414113,18.3303168 C2.23800371,14.8341794 2.23800371,9.16582064 5.73414113,5.66968323 C9.05475132,2.34907304 14.3349409,2.18235834 17.8523166,5.16953912 L17.8521761,4.08577298 C17.8521761,3.66197713 18.1957305,3.31842271 18.6195264,3.31842271 Z">
</Setter>
</Style>
</Grid.Styles>
</Grid>
</ControlTemplate>
</Setter>
</ControlTheme>
</ResourceDictionary>

3
src/Avalonia.Themes.Simple/Accents/BaseDark.xaml

@ -31,5 +31,8 @@
<SolidColorBrush x:Key="ThemeControlHighlightHighBrush" Color="{StaticResource ThemeControlHighlightHighColor}" />
<SolidColorBrush x:Key="ThemeForegroundBrush" Color="{StaticResource ThemeForegroundColor}" />
<SolidColorBrush x:Key="HighlightBrush" Color="{StaticResource HighlightColor}" />
<SolidColorBrush x:Key="RefreshVisualizerForeground" Color="White"/>
<SolidColorBrush x:Key="RefreshVisualizerBackground" Color="Transparent"/>
</ResourceDictionary>

4
src/Avalonia.Themes.Simple/Accents/BaseLight.xaml

@ -31,5 +31,7 @@
<SolidColorBrush x:Key="ThemeControlHighlightHighBrush" Color="{StaticResource ThemeControlHighlightHighColor}" />
<SolidColorBrush x:Key="ThemeForegroundBrush" Color="{StaticResource ThemeForegroundColor}" />
<SolidColorBrush x:Key="HighlightBrush" Color="{StaticResource HighlightColor}" />
<SolidColorBrush x:Key="HighlightBrush" Color="{StaticResource HighlightColor}" />
<SolidColorBrush x:Key="RefreshVisualizerForeground" Color="Black"/>
<SolidColorBrush x:Key="RefreshVisualizerBackground" Color="Transparent"/>
</ResourceDictionary>

24
src/Avalonia.Themes.Simple/Controls/RefreshContainer.xaml

@ -0,0 +1,24 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ControlTheme x:Key="{x:Type RefreshContainer}"
TargetType="RefreshContainer">
<Setter Property="Template">
<ControlTemplate>
<Grid>
<ContentPresenter Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
Padding="{TemplateBinding Padding}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}">
</ContentPresenter>
<Grid Name="PART_RefreshVisualizerPresenter"/>
</Grid>
</ControlTemplate>
</Setter>
</ControlTheme>
</ResourceDictionary>

31
src/Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml

@ -0,0 +1,31 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ControlTheme x:Key="{x:Type RefreshVisualizer}"
TargetType="RefreshVisualizer">
<Setter Property="IsTabStop"
Value="False"/>
<Setter Property="IsHitTestVisible"
Value="False"/>
<Setter Property="Height"
Value="100"/>
<Setter Property="Background"
Value="{DynamicResource RefreshVisualizerBackground}"/>
<Setter Property="Foreground"
Value="{DynamicResource RefreshVisualizerForeground}"/>
<Setter Property="Template">
<ControlTemplate>
<Grid Name="PART_Root"
MinHeight="80"
Background="{TemplateBinding Background}">
<Grid.Styles>
<Style Selector="PathIcon#PART_Icon">
<Setter Property="Data"
Value="M18.6195264,3.31842271 C19.0080059,3.31842271 19.3290603,3.60710385 19.3798716,3.9816481 L19.3868766,4.08577298 L19.3868766,6.97963208 C19.3868766,7.36811161 19.0981955,7.68916605 18.7236513,7.73997735 L18.6195264,7.74698235 L15.7256673,7.74698235 C15.3018714,7.74698235 14.958317,7.40342793 14.958317,6.97963208 C14.958317,6.59115255 15.2469981,6.27009811 15.6215424,6.21928681 L15.7256673,6.21228181 L16.7044011,6.21182461 C13.7917384,3.87107476 9.52212532,4.05209336 6.81933829,6.75488039 C3.92253872,9.65167996 3.92253872,14.34832 6.81933829,17.2451196 C9.71613786,20.1419192 14.4127779,20.1419192 17.3095775,17.2451196 C19.0725398,15.4821573 19.8106555,12.9925923 19.3476248,10.58925 C19.2674502,10.173107 19.5398064,9.77076216 19.9559494,9.69058758 C20.3720923,9.610413 20.7744372,9.88276918 20.8546118,10.2989121 C21.4129973,13.1971899 20.5217103,16.2033812 18.3947747,18.3303168 C14.8986373,21.8264542 9.23027854,21.8264542 5.73414113,18.3303168 C2.23800371,14.8341794 2.23800371,9.16582064 5.73414113,5.66968323 C9.05475132,2.34907304 14.3349409,2.18235834 17.8523166,5.16953912 L17.8521761,4.08577298 C17.8521761,3.66197713 18.1957305,3.31842271 18.6195264,3.31842271 Z">
</Setter>
</Style>
</Grid.Styles>
</Grid>
</ControlTemplate>
</Setter>
</ControlTheme>
</ResourceDictionary>

2
src/Avalonia.Themes.Simple/Controls/SimpleControls.xaml

@ -65,6 +65,8 @@
<ResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/ManagedFileChooser.xaml" />
<ResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/SplitButton.xaml" />
<ResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/SelectableTextBlock.xaml" />
<ResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/RefreshContainer.xaml" />
<ResourceInclude Source="avares://Avalonia.Themes.Simple/Controls/RefreshVisualizer.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Styles.Resources>

2
src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs

@ -234,7 +234,7 @@ namespace Avalonia.Markup.Xaml.XamlIl
parsedDocuments.Add(new XamlDocumentResource(parsed, document.BaseUri?.ToString(), null, null,
builder,
compiler.DefinePopulateMethod(builder, parsed, AvaloniaXamlIlCompiler.PopulateName, true),
compiler.DefineBuildMethod(builder, parsed, AvaloniaXamlIlCompiler.BuildName, true)));
document.RootInstance is null ? compiler.DefineBuildMethod(builder, parsed, AvaloniaXamlIlCompiler.BuildName, true) : null));
originalDocuments.Add(document);
}

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

@ -1 +1 @@
Subproject commit 491de981dd4433ee58bc9540e2cd4a5d168f8168
Subproject commit 5e498f8bcca403a34aff5efc825cbb4e12b7fa8e

9
src/Windows/Avalonia.Win32/TrayIconImpl.cs

@ -19,7 +19,6 @@ namespace Avalonia.Win32
public class TrayIconImpl : ITrayIconImpl
{
private static readonly IntPtr s_emptyIcon = new System.Drawing.Bitmap(32, 32).GetHicon();
private readonly int _uniqueId;
private static int s_nextUniqueId;
private bool _iconAdded;
@ -137,6 +136,12 @@ namespace Avalonia.Win32
private void OnRightClicked()
{
var menuItems = _exporter.GetMenu();
if (null == menuItems || menuItems.Count == 0)
{
return;
}
var _trayMenu = new TrayPopupRoot()
{
SystemDecorations = SystemDecorations.None,
@ -145,7 +150,7 @@ namespace Avalonia.Win32
TransparencyLevelHint = WindowTransparencyLevel.Transparent,
Content = new TrayIconMenuFlyoutPresenter()
{
Items = _exporter.GetMenu()
Items = menuItems
}
};

5
tests/Avalonia.Base.UnitTests/Animation/AnimationIterationTests.cs

@ -181,7 +181,7 @@ namespace Avalonia.Base.UnitTests.Animation
Assert.Equal(border.Width, 300d);
}
[Fact(Skip = "See #6111")]
[Fact]
public void Dispose_Subscription_Should_Stop_Animation()
{
var keyframe1 = new KeyFrame()
@ -310,7 +310,7 @@ namespace Avalonia.Base.UnitTests.Animation
Assert.True(animationRun.IsCompleted);
}
[Fact(Skip = "See #6111")]
[Fact]
public void Cancellation_Should_Stop_Animation()
{
var keyframe1 = new KeyFrame()
@ -372,7 +372,6 @@ namespace Avalonia.Base.UnitTests.Animation
clock.Step(TimeSpan.FromSeconds(1));
clock.Step(TimeSpan.FromSeconds(2));
clock.Step(TimeSpan.FromSeconds(3));
//Assert.Equal(2, propertyChangedCount);
animationRun.Wait();

27
tests/Avalonia.Controls.UnitTests/FlowDirectionTests.cs → tests/Avalonia.Base.UnitTests/FlowDirectionTests.cs

@ -8,7 +8,7 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void HasMirrorTransform_Should_Be_True()
{
var target = new Control
var target = new Visual
{
FlowDirection = FlowDirection.RightToLeft,
};
@ -19,31 +19,36 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void HasMirrorTransform_Of_LTR_Children_Should_Be_True_For_RTL_Parent()
{
Control child;
var target = new Decorator
var child = new Visual()
{
FlowDirection = FlowDirection.LeftToRight,
};
var target = new Visual
{
FlowDirection = FlowDirection.RightToLeft,
Child = child = new Control()
};
target.VisualChildren.Add(child);
child.FlowDirection = FlowDirection.LeftToRight;
child.InvalidateMirrorTransform();
Assert.True(target.HasMirrorTransform);
Assert.True(child.HasMirrorTransform);
}
[Fact]
public void HasMirrorTransform_Of_Children_Is_Updated_After_Parent_Changeed()
public void HasMirrorTransform_Of_Children_Is_Updated_After_Parent_Changed()
{
Control child;
var child = new Visual()
{
FlowDirection = FlowDirection.LeftToRight,
};
var target = new Decorator
{
FlowDirection = FlowDirection.LeftToRight,
Child = child = new Control()
{
FlowDirection = FlowDirection.LeftToRight,
}
};
target.VisualChildren.Add(child);
Assert.False(target.HasMirrorTransform);
Assert.False(child.HasMirrorTransform);

29
tests/Avalonia.Base.UnitTests/Input/InputElement_Focus.cs

@ -1,5 +1,7 @@
using System;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.UnitTests;
using Xunit;
@ -25,6 +27,33 @@ namespace Avalonia.Base.UnitTests.Input
}
}
[Fact]
public void Focus_Should_Not_Get_Restored_To_Enabled_Control()
{
using (UnitTestApplication.Start(TestServices.RealFocus))
{
var sp = new StackPanel();
Button target = new Button();
Button target1 = new Button();
target.Click += (s, e) => target.IsEnabled = false;
target1.Click += (s, e) => target.IsEnabled = true;
sp.Children.Add(target);
sp.Children.Add(target1);
var root = new TestRoot
{
Child = sp
};
target.Focus();
target.RaiseEvent(new RoutedEventArgs(AccessKeyHandler.AccessKeyPressedEvent));
Assert.False(target.IsEnabled);
Assert.False(target.IsFocused);
target1.RaiseEvent(new RoutedEventArgs(AccessKeyHandler.AccessKeyPressedEvent));
Assert.True(target.IsEnabled);
Assert.False(target.IsFocused);
}
}
[Fact]
public void Focus_Should_Be_Cleared_When_Control_Is_Removed_From_VisualTree()
{

48
tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs

@ -868,7 +868,53 @@ namespace Avalonia.Base.UnitTests.Styling
}
[Fact]
public void Animations_Should_Be_Activated_And_Deactivated()
public void Animations_Should_Be_Activated()
{
Style style = new Style(x => x.OfType<Class1>())
{
Animations =
{
new Avalonia.Animation.Animation
{
Duration = TimeSpan.FromSeconds(1),
Children =
{
new KeyFrame
{
Setters =
{
new Setter { Property = Class1.DoubleProperty, Value = 5.0 }
},
},
new KeyFrame
{
Setters =
{
new Setter { Property = Class1.DoubleProperty, Value = 10.0 }
},
Cue = new Cue(1d)
}
},
}
}
};
var clock = new TestClock();
var target = new Class1 { Clock = clock };
StyleHelpers.TryAttach(style, target);
Assert.Equal(0.0, target.Double);
clock.Step(TimeSpan.Zero);
Assert.Equal(5.0, target.Double);
clock.Step(TimeSpan.FromSeconds(0.5));
Assert.Equal(7.5, target.Double);
}
[Fact]
public void Animations_With_Trigger_Should_Be_Activated_And_Deactivated()
{
Style style = new Style(x => x.OfType<Class1>().Class("foo"))
{

51
tests/Avalonia.Base.UnitTests/Styling/StyledElementTests_Theming.cs

@ -359,16 +359,49 @@ public class StyledElementTests_Theming
}
[Fact]
public void Implicit_Theme_Is_Cleared_When_Removed_From_Logical_Tree()
public void Implicit_Theme_Is_Not_Detached_When_Removed_From_Logical_Tree()
{
var target = CreateTarget();
var root = CreateRoot(target);
Assert.NotNull(target.GetEffectiveTheme());
Assert.Equal("theme", target.Tag);
root.Child = null;
Assert.Null(target.GetEffectiveTheme());
var border = Assert.IsType<Border>(target.VisualChild);
Assert.Equal("theme", target.Tag);
Assert.Equal("theme", border.Tag);
}
[Fact]
public void Can_Attach_Then_Reattach_To_Same_Logical_Tree()
{
var target = CreateTarget();
var root = CreateRoot(target);
Assert.Equal("theme", target.Tag);
root.Child = null;
root.Child = target;
Assert.Equal("theme", target.Tag);
}
[Fact]
public void Implicit_Theme_Is_Reevaluated_When_Removed_And_Added_To_Different_Logical_Tree()
{
var target = CreateTarget();
var root1 = CreateRoot(target, "theme1");
var root2 = CreateRoot(null, "theme2");
Assert.Equal("theme1", target.Tag);
root1.Child = null;
root2.Child = target;
var border = Assert.IsType<Border>(target.VisualChild);
Assert.Equal("theme2", target.Tag);
Assert.Equal("theme2", border.Tag);
}
[Fact]
@ -402,10 +435,10 @@ public class StyledElementTests_Theming
private static ThemedControl CreateTarget() => new ThemedControl();
private static TestRoot CreateRoot(Control child)
private static TestRoot CreateRoot(Control? child, string themeTag = "theme")
{
var result = new TestRoot();
result.Resources.Add(typeof(ThemedControl), CreateTheme());
result.Resources.Add(typeof(ThemedControl), CreateTheme(themeTag));
result.Child = child;
result.LayoutManager.ExecuteInitialLayoutPass();
return result;
@ -530,7 +563,7 @@ public class StyledElementTests_Theming
}
}
private static ControlTheme CreateTheme()
private static ControlTheme CreateTheme(string tag = "theme")
{
var template = new FuncControlTemplate<ThemedControl>((o, n) => new Border());
@ -539,7 +572,7 @@ public class StyledElementTests_Theming
TargetType = typeof(ThemedControl),
Setters =
{
new Setter(Control.TagProperty, "theme"),
new Setter(Control.TagProperty, tag),
new Setter(TemplatedControl.TemplateProperty, template),
new Setter(TemplatedControl.CornerRadiusProperty, new CornerRadius(5)),
},
@ -550,7 +583,7 @@ public class StyledElementTests_Theming
Setters =
{
new Setter(Border.BackgroundProperty, Brushes.Red),
new Setter(Control.TagProperty, "theme"),
new Setter(Control.TagProperty, tag),
}
},
new Style(x => x.Nesting().Class("foo").Template().OfType<Border>())

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

@ -59,6 +59,28 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact]
public void TextBox_Should_Lose_Focus_When_Disabled()
{
using (UnitTestApplication.Start(FocusServices))
{
var target = new TextBox
{
Template = CreateTemplate()
};
target.ApplyTemplate();
var root = new TestRoot() { Child = target };
target.Focus();
Assert.True(target.IsFocused);
target.IsEnabled = false;
Assert.False(target.IsFocused);
Assert.False(target.IsEnabled);
}
}
[Fact]
public void Opening_Context_Flyout_Does_not_Lose_Selection()
{

47
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@ -403,7 +403,7 @@ namespace Avalonia.Controls.UnitTests
parent.Close();
var ex = Assert.Throws<InvalidOperationException>(() => target.Show(parent));
Assert.Equal("Cannot show a window with a closed parent.", ex.Message);
Assert.Equal("Cannot show a window with a closed owner.", ex.Message);
}
}
@ -431,7 +431,7 @@ namespace Avalonia.Controls.UnitTests
var target = new Window();
var ex = Assert.Throws<InvalidOperationException>(() => target.Show(parent));
Assert.Equal("Cannot show window with non-visible parent.", ex.Message);
Assert.Equal("Cannot show window with non-visible owner.", ex.Message);
}
}
@ -444,7 +444,7 @@ namespace Avalonia.Controls.UnitTests
var target = new Window();
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => target.ShowDialog(parent));
Assert.Equal("Cannot show window with non-visible parent.", ex.Message);
Assert.Equal("Cannot show window with non-visible owner.", ex.Message);
}
}
@ -456,7 +456,7 @@ namespace Avalonia.Controls.UnitTests
var target = new Window();
var ex = Assert.Throws<InvalidOperationException>(() => target.Show(target));
Assert.Equal("A Window cannot be its own parent.", ex.Message);
Assert.Equal("A Window cannot be its own owner.", ex.Message);
}
}
@ -986,7 +986,46 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent);
}
}
[Fact]
public void IsVisible_Should_Open_Window()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var target = new Window();
var raised = false;
target.Opened += (s, e) => raised = true;
target.IsVisible = true;
Assert.True(raised);
}
}
[Fact]
public void IsVisible_Should_Close_DialogWindow()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var parent = new Window();
parent.Show();
var target = new Window();
var raised = false;
var task = target.ShowDialog<bool>(parent);
target.Closed += (sender, args) => raised = true;
target.IsVisible = false;
Assert.True(raised);
Assert.False(task.Result);
}
}
protected virtual void Show(Window window)
{
window.Show();

16
tests/Avalonia.IntegrationTests.Appium/WindowTests.cs

@ -90,7 +90,7 @@ namespace Avalonia.IntegrationTests.Appium
try
{
_session.FindElementByAccessibilityId("WindowState").SendClick();
_session.FindElementByName("Normal").SendClick();
_session.FindElementByAccessibilityId("WindowStateNormal").SendClick();
// Wait for animations to run.
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
@ -155,11 +155,11 @@ namespace Avalonia.IntegrationTests.Appium
Assert.Equal("Normal", windowState.GetComboBoxValue());
windowState.Click();
_session.FindElementByName("Maximized").SendClick();
_session.FindElementByAccessibilityId("WindowStateMaximized").SendClick();
Assert.Equal("Maximized", windowState.GetComboBoxValue());
windowState.Click();
_session.FindElementByName("Normal").SendClick();
_session.FindElementByAccessibilityId("WindowStateNormal").SendClick();
var current = GetWindowInfo();
Assert.Equal(original.Position, current.Position);
@ -169,7 +169,7 @@ namespace Avalonia.IntegrationTests.Appium
if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || mode == ShowWindowMode.NonOwned)
{
windowState.Click();
_session.FindElementByName("FullScreen").SendClick();
_session.FindElementByAccessibilityId("WindowStateFullScreen").SendClick();
Assert.Equal("FullScreen", windowState.GetComboBoxValue());
current = GetWindowInfo();
@ -179,7 +179,7 @@ namespace Avalonia.IntegrationTests.Appium
windowState.SendClick();
_session.FindElementByName("Normal").SendClick();
_session.FindElementByAccessibilityId("WindowStateNormal").SendClick();
current = GetWindowInfo();
Assert.Equal(original.Position, current.Position);
@ -223,13 +223,13 @@ namespace Avalonia.IntegrationTests.Appium
// Not sure how to handle testing minimized windows currently.
if (state == Controls.WindowState.Minimized)
continue;
// Child/Modal windows cannot be fullscreen on macOS.
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) &&
state == Controls.WindowState.FullScreen &&
mode != ShowWindowMode.NonOwned)
continue;
data.Add(size, mode, state);
}
}
@ -286,7 +286,7 @@ namespace Avalonia.IntegrationTests.Appium
_session.FindElementByName(location.ToString()).SendClick();
stateComboBox.Click();
_session.FindElementByName(state.ToString()).SendClick();
_session.FindElementByAccessibilityId($"ShowWindowState{state}").SendClick();
return showButton.OpenWindowWithClick();
}

74
tests/Avalonia.IntegrationTests.Appium/WindowTests_MacOS.cs

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Avalonia.Controls;
using Avalonia.Utilities;
using OpenQA.Selenium;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Interactions;
@ -114,6 +115,79 @@ namespace Avalonia.IntegrationTests.Appium
Assert.Equal(1, secondaryWindowIndex);
}
}
[PlatformFact(TestPlatforms.MacOS)]
public void WindowOrder_Owned_Dialog_Stays_InFront_Of_FullScreen_Parent()
{
var mainWindow = _session.FindElementByAccessibilityId("MainWindow");
// Enter fullscreen
mainWindow.FindElementByAccessibilityId("EnterFullscreen").Click();
// Wait for fullscreen transition.
Thread.Sleep(1000);
// Make sure we entered fullscreen.
var windowState = mainWindow.FindElementByAccessibilityId("MainWindowState");
Assert.Equal("FullScreen", windowState.Text);
// Open child window.
using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Owned, WindowStartupLocation.Manual))
{
mainWindow.SendClick();
var secondaryWindowIndex = GetWindowOrder("SecondaryWindow");
Assert.Equal(1, secondaryWindowIndex);
}
// Exit fullscreen by menu shortcut Command+R
mainWindow.FindElementByAccessibilityId("ExitFullscreen").Click();
// Wait for restore transition.
Thread.Sleep(1000);
// Make sure we exited fullscreen.
mainWindow = _session.FindElementByAccessibilityId("MainWindow");
windowState = mainWindow.FindElementByAccessibilityId("MainWindowState");
Assert.Equal("Normal", windowState.Text);
}
[PlatformFact(TestPlatforms.MacOS)]
public void Does_Not_Switch_Space_From_FullScreen_To_Main_Desktop_When_FullScreen_Window_Clicked()
{
// Issue #9565
var mainWindow = _session.FindElementByAccessibilityId("MainWindow");
AppiumWebElement windowState;
// Open child window.
using (OpenWindow(new PixelSize(200, 100), ShowWindowMode.Owned, WindowStartupLocation.Manual))
{
// Enter fullscreen
mainWindow.FindElementByAccessibilityId("EnterFullscreen").Click();
// Wait for fullscreen transition.
Thread.Sleep(1000);
// Make sure we entered fullscreen.
mainWindow = _session.FindElementByAccessibilityId("MainWindow");
windowState = mainWindow.FindElementByAccessibilityId("MainWindowState");
Assert.Equal("FullScreen", windowState.Text);
// Click on main window
mainWindow.Click();
// Failed here due to #9565: main window is no longer visible as the main space is now shown instead
// of the fullscreen space.
mainWindow.FindElementByAccessibilityId("ExitFullscreen").Click();
// Wait for restore transition.
Thread.Sleep(1000);
}
// Make sure we exited fullscreen.
mainWindow = _session.FindElementByAccessibilityId("MainWindow");
windowState = mainWindow.FindElementByAccessibilityId("MainWindowState");
Assert.Equal("Normal", windowState.Text);
}
[PlatformFact(TestPlatforms.MacOS)]
public void WindowOrder_NonOwned_Window_Does_Not_Stay_InFront_Of_Parent()

23
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs

@ -899,6 +899,17 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.Equal("Foo", target.Text);
}
[Fact]
public void Should_Parse_And_Populate_Type_Without_Public_Ctor()
{
var xaml = @"<ObjectWithoutPublicCtor xmlns='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml' Test2='World' />";
var target = (ObjectWithoutPublicCtor)AvaloniaRuntimeXamlLoader.Load(xaml, rootInstance: new ObjectWithoutPublicCtor("Hello"));
Assert.NotNull(target);
Assert.Equal("World", target.Test2);
Assert.Equal("Hello", target.Test1);
}
private class SelectedItemsViewModel : INotifyPropertyChanged
{
public string[] Items { get; set; }
@ -928,6 +939,18 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Child = child;
}
}
public class ObjectWithoutPublicCtor
{
public ObjectWithoutPublicCtor(string param)
{
Test1 = param;
}
public string Test1 { get; set; }
public string Test2 { get; set; }
}
public class ObjectWithAddChildOfT : IAddChild, IAddChild<string>
{

Loading…
Cancel
Save