Browse Source

Merge remote-tracking branch 'origin/master' into feature/fluent-progress-bar

# Conflicts:
#	src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml
pull/4074/head
Dan Walmsley 6 years ago
parent
commit
a83105a99e
  1. 33
      readme.md
  2. 6
      samples/ControlCatalog/Models/Person.cs
  3. 2
      samples/ControlCatalog/Pages/DialogsPage.xaml
  4. 15
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  5. 12
      samples/ControlCatalog/Pages/NumericUpDownPage.xaml
  6. 2
      samples/ControlCatalog/Pages/ToolTipPage.xaml
  7. 3
      samples/RenderDemo/MainWindow.xaml
  8. 115
      samples/RenderDemo/Pages/TransitionsPage.xaml
  9. 37
      samples/RenderDemo/Pages/TransitionsPage.xaml.cs
  10. 36
      src/Avalonia.Animation/TransitionInstance.cs
  11. 9
      src/Avalonia.Animation/Transition`1.cs
  12. 1
      src/Avalonia.Base/Properties/AssemblyInfo.cs
  13. 47
      src/Avalonia.Base/Utilities/MathUtilities.cs
  14. 38
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  15. 2
      src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
  16. 16
      src/Avalonia.Controls.DataGrid/DataGridColumns.cs
  17. 2
      src/Avalonia.Controls.DataGrid/DataGridLength.cs
  18. 2
      src/Avalonia.Controls.DataGrid/DataGridRow.cs
  19. 60
      src/Avalonia.Controls.DataGrid/DataGridRows.cs
  20. 6
      src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs
  21. 136
      src/Avalonia.Controls.DataGrid/Utils/DoubleUtil.cs
  22. 167
      src/Avalonia.Controls/ContextMenu.cs
  23. 22
      src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs
  24. 64
      src/Avalonia.Controls/Converters/MenuScrollingVisibilityConverter.cs
  25. 6
      src/Avalonia.Controls/LayoutTransformControl.cs
  26. 5
      src/Avalonia.Controls/MenuItem.cs
  27. 5
      src/Avalonia.Controls/Platform/IWindowBaseImpl.cs
  28. 2
      src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs
  29. 55
      src/Avalonia.Controls/Primitives/IPopupHost.cs
  30. 12
      src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
  31. 117
      src/Avalonia.Controls/Primitives/Popup.cs
  32. 424
      src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
  33. 117
      src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs
  34. 6
      src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs
  35. 23
      src/Avalonia.Controls/Primitives/PopupRoot.cs
  36. 2
      src/Avalonia.Controls/Primitives/Track.cs
  37. 32
      src/Avalonia.Controls/ScrollViewer.cs
  38. 2
      src/Avalonia.Controls/Slider.cs
  39. 32
      src/Avalonia.Controls/TextBox.cs
  40. 105
      src/Avalonia.Controls/ToggleSwitch.cs
  41. 3
      src/Avalonia.Controls/TopLevel.cs
  42. 95
      src/Avalonia.Controls/Window.cs
  43. 2
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
  44. 2
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  45. 5
      src/Avalonia.Layout/ILayoutRoot.cs
  46. 3
      src/Avalonia.Native/OsxManagedPopupPositionerPopupImplHelper.cs
  47. 2
      src/Avalonia.Native/WindowImplBase.cs
  48. 2
      src/Avalonia.Themes.Default/ContextMenu.xaml
  49. 1
      src/Avalonia.Themes.Default/DefaultTheme.xaml
  50. 2
      src/Avalonia.Themes.Default/MenuItem.xaml
  51. 150
      src/Avalonia.Themes.Default/ScrollViewer.xaml
  52. 2
      src/Avalonia.Themes.Default/Slider.xaml
  53. 294
      src/Avalonia.Themes.Default/ToggleSwitch.xaml
  54. 3
      src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml
  55. 3
      src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml
  56. 157
      src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml
  57. 156
      src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml
  58. 8
      src/Avalonia.Themes.Fluent/Button.xaml
  59. 200
      src/Avalonia.Themes.Fluent/ButtonSpinner.xaml
  60. 6
      src/Avalonia.Themes.Fluent/Common.xaml
  61. 69
      src/Avalonia.Themes.Fluent/ContextMenu.xaml
  62. 1
      src/Avalonia.Themes.Fluent/FluentTheme.xaml
  63. 28
      src/Avalonia.Themes.Fluent/Menu.xaml
  64. 244
      src/Avalonia.Themes.Fluent/MenuItem.xaml
  65. 52
      src/Avalonia.Themes.Fluent/NumericUpDown.xaml
  66. 2
      src/Avalonia.Themes.Fluent/RepeatButton.xaml
  67. 12
      src/Avalonia.Themes.Fluent/Separator.xaml
  68. 1
      src/Avalonia.Themes.Fluent/TextBox.xaml
  69. 1
      src/Avalonia.Themes.Fluent/ToggleButton.xaml
  70. 294
      src/Avalonia.Themes.Fluent/ToggleSwitch.xaml
  71. 10
      src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs
  72. 35
      src/Avalonia.Visuals/Animation/Animators/TransformOperationsAnimator.cs
  73. 28
      src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs
  74. 75
      src/Avalonia.Visuals/Matrix.cs
  75. 128
      src/Avalonia.Visuals/Media/Color.cs
  76. 12
      src/Avalonia.Visuals/Media/IMutableTransform.cs
  77. 10
      src/Avalonia.Visuals/Media/ITransform.cs
  78. 5
      src/Avalonia.Visuals/Media/Transform.cs
  79. 23
      src/Avalonia.Visuals/Media/TransformConverter.cs
  80. 40
      src/Avalonia.Visuals/Media/Transformation/InterpolationUtilities.cs
  81. 230
      src/Avalonia.Visuals/Media/Transformation/TransformOperation.cs
  82. 258
      src/Avalonia.Visuals/Media/Transformation/TransformOperations.cs
  83. 444
      src/Avalonia.Visuals/Media/Transformation/TransformParser.cs
  84. 1
      src/Avalonia.Visuals/Properties/AssemblyInfo.cs
  85. 13
      src/Avalonia.Visuals/Size.cs
  86. 52
      src/Avalonia.Visuals/Vector.cs
  87. 14
      src/Avalonia.Visuals/Visual.cs
  88. 2
      src/Avalonia.Visuals/VisualTree/IVisual.cs
  89. 2
      src/Avalonia.X11/X11Window.cs
  90. 18
      src/Avalonia.X11/XI2Manager.cs
  91. 26
      src/Windows/Avalonia.Win32/PopupImpl.cs
  92. 2
      src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs
  93. 16
      src/Windows/Avalonia.Win32/WindowImpl.cs
  94. 10
      tests/Avalonia.Animation.UnitTests/TestClock.cs
  95. 88
      tests/Avalonia.Animation.UnitTests/TransitionsTests.cs
  96. 109
      tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs
  97. 16
      tests/Avalonia.Benchmarks/Visuals/MatrixBenchmarks.cs
  98. 20
      tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs
  99. 15
      tests/Avalonia.Controls.UnitTests/WindowTests.cs
  100. 2
      tests/Avalonia.UnitTests/MockWindowingPlatform.cs

33
readme.md

@ -1,24 +1,20 @@
<img src='https://avatars2.githubusercontent.com/u/14075148?s=200&v=4' width='100' />
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) [![Build Status](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_apis/build/status/AvaloniaUI.Avalonia)](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) [![Backers on Open Collective](https://opencollective.com/Avalonia/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/Avalonia/sponsors/badge.svg)](#sponsors) ![License](https://img.shields.io/github/license/avaloniaui/avalonia.svg)
<br />
[![NuGet](https://img.shields.io/nuget/v/Avalonia.svg)](https://www.nuget.org/packages/Avalonia) [![downloads](https://img.shields.io/nuget/dt/avalonia)](https://www.nuget.org/packages/Avalonia) [![MyGet](https://img.shields.io/myget/avalonia-ci/vpre/Avalonia.svg?label=myget)](https://www.myget.org/gallery/avalonia-ci) ![Size](https://img.shields.io/github/repo-size/avaloniaui/avalonia.svg)
# Avalonia
<img alt="Avalonia" src="https://user-images.githubusercontent.com/6759207/84897744-cab6d800-b0ae-11ea-8214-e5174d71f5c8.png" width="400"/>
| Gitter Chat | Build Status (Win, Linux, OSX) | Open Collective | NuGet | MyGet |
|---|---|---|---|---|
| [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) | [![Build Status](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_apis/build/status/AvaloniaUI.Avalonia)](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) | [![Backers on Open Collective](https://opencollective.com/Avalonia/backers/badge.svg)](#backers) [![Sponsors on Open Collective](https://opencollective.com/Avalonia/sponsors/badge.svg)](#sponsors) | [![NuGet](https://img.shields.io/nuget/v/Avalonia.svg)](https://www.nuget.org/packages/Avalonia) | [![MyGet](https://img.shields.io/myget/avalonia-ci/vpre/Avalonia.svg?label=myget)](https://www.myget.org/gallery/avalonia-ci) |
## 📖 About AvaloniaUI
## About
Avalonia is a cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows via .NET Framework and .NET Core, Linux via Xorg, macOS. Avalonia is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and breaking changes as we continue along into this project's development.
**Avalonia** is a cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows (.NET Framework, .NET Core), Linux (via Xorg), macOS.
<img src="https://user-images.githubusercontent.com/6759207/84751662-7c79da00-afc5-11ea-8780-dda28db70b76.png" width="700" />
**Avalonia** is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and breaking changes as we continue along into this project's development.
> **Note:** The UI theme you see in the picture above is still work-in-progress and will be available in the upcoming Avalonia 0.10.0 release. However, you can connect to our nightly build feed and install latest pre-release versions of Avalonia NuGet packages, if you are willing to help out with the development and testing. See [Using nightly build feed](https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed) for more info.
To see the status of some of our features, please see our [Roadmap here](https://github.com/AvaloniaUI/Avalonia/issues/2239).
To see the status of some of our features, please see our [Roadmap](https://github.com/AvaloniaUI/Avalonia/issues/2239). You can also see what [breaking changes](https://github.com/AvaloniaUI/Avalonia/issues/3538) we have planned and what our [past breaking changes](https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes) have been. [Awesome Avalonia](https://github.com/AvaloniaCommunity/awesome-avalonia) is community-curated list of awesome Avalonia UI tools, libraries, projects and resources. Go and see what people are building with Avalonia!
You can also see what [breaking changes](https://github.com/AvaloniaUI/Avalonia/issues/3538) we have planned and what our [past breaking changes](https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes) have been.
[Awesome Avalonia](https://github.com/AvaloniaCommunity/awesome-avalonia) is community-curated list of awesome Avalonia UI tools, libraries, projects and resources. Go and see what people are building with Avalonia!
## Getting Started
## 🚀 Getting Started
The Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started, or you can use the .NET Core CLI. For a starer guide see our [documentation](http://avaloniaui.net/docs/quickstart/create-new-project).
@ -30,6 +26,15 @@ Install-Package Avalonia
Install-Package Avalonia.Desktop
```
## Showcase
Examples of UIs built with Avalonia
![image](https://user-images.githubusercontent.com/4672627/84707589-5b69a880-af35-11ea-87a6-7ad57a31d314.png)
![image](https://user-images.githubusercontent.com/4672627/84708576-28281900-af37-11ea-8c88-e29dfcfa0558.png)
![image](https://user-images.githubusercontent.com/4672627/84708947-c3b98980-af37-11ea-8c9d-503334615bbf.png)
## JetBrains Rider
If you need to develop Avalonia app with JetBrains Rider, go and *vote* on [this issue](https://youtrack.jetbrains.com/issue/RIDER-39247) in their tracker. JetBrains won't do things without their users telling them that they want the feature, so only **YOU** can make it happen.

6
samples/ControlCatalog/Models/Person.cs

@ -21,12 +21,12 @@ namespace ControlCatalog.Models
get => _firstName;
set
{
_firstName = value;
if (string.IsNullOrWhiteSpace(value))
SetError(nameof(FirstName), "First Name Required");
else
SetError(nameof(FirstName), null);
_firstName = value;
OnPropertyChanged(nameof(FirstName));
}
@ -37,12 +37,12 @@ namespace ControlCatalog.Models
get => _lastName;
set
{
_lastName = value;
if (string.IsNullOrWhiteSpace(value))
SetError(nameof(LastName), "Last Name Required");
else
SetError(nameof(LastName), null);
_lastName = value;
OnPropertyChanged(nameof(LastName));
}
}
@ -95,4 +95,4 @@ namespace ControlCatalog.Models
return null;
}
}
}
}

2
samples/ControlCatalog/Pages/DialogsPage.xaml

@ -11,5 +11,7 @@
<Button Name="DecoratedWindowDialog">Decorated window (dialog)</Button>
<Button Name="Dialog">Dialog</Button>
<Button Name="DialogNoTaskbar">Dialog (No taskbar icon)</Button>
<Button Name="OwnedWindow">Owned window</Button>
<Button Name="OwnedWindowNoTaskbar">Owned window (No taskbar icon)</Button>
</StackPanel>
</UserControl>

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

@ -93,6 +93,21 @@ namespace ControlCatalog.Pages
window.ShowInTaskbar = false;
window.ShowDialog(GetWindow());
};
this.FindControl<Button>("OwnedWindow").Click += delegate
{
var window = CreateSampleWindow();
window.Show(GetWindow());
};
this.FindControl<Button>("OwnedWindowNoTaskbar").Click += delegate
{
var window = CreateSampleWindow();
window.ShowInTaskbar = false;
window.Show(GetWindow());
};
}
private Window CreateSampleWindow()

12
samples/ControlCatalog/Pages/NumericUpDownPage.xaml

@ -50,22 +50,22 @@
<TextBlock Grid.Row="4" Grid.Column="0" VerticalAlignment="Center" Margin="2">Text:</TextBlock>
<TextBox Grid.Row="4" Grid.Column="1" Text="{Binding #upDown.Text}" VerticalAlignment="Center" Margin="2" />
</Grid>
<Grid Grid.Row="0" Grid.Column="2" Margin="10,2,2,2" RowDefinitions="Auto,Auto,Auto,Auto,Auto" ColumnDefinitions="Auto, 120">
<Grid Grid.Row="0" Grid.Column="2" Margin="10,2,2,2" RowDefinitions="Auto,Auto,Auto,Auto,Auto" ColumnDefinitions="Auto, Auto">
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Minimum:</TextBlock>
<NumericUpDown Grid.Row="0" Grid.Column="1" Value="{Binding #upDown.Minimum}"
CultureInfo="{Binding #upDown.CultureInfo}" VerticalAlignment="Center" Margin="2" Width="70" HorizontalAlignment="Center"/>
CultureInfo="{Binding #upDown.CultureInfo}" VerticalAlignment="Center" Margin="2" HorizontalAlignment="Center"/>
<TextBlock Grid.Row="1" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Maximum:</TextBlock>
<NumericUpDown Grid.Row="1" Grid.Column="1" Value="{Binding #upDown.Maximum}"
CultureInfo="{Binding #upDown.CultureInfo}" VerticalAlignment="Center" Margin="2" Width="70" HorizontalAlignment="Center"/>
CultureInfo="{Binding #upDown.CultureInfo}" VerticalAlignment="Center" Margin="2" HorizontalAlignment="Center"/>
<TextBlock Grid.Row="2" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Increment:</TextBlock>
<NumericUpDown Grid.Row="2" Grid.Column="1" Value="{Binding #upDown.Increment}" VerticalAlignment="Center"
Margin="2" Width="70" HorizontalAlignment="Center"/>
Margin="2" HorizontalAlignment="Center"/>
<TextBlock Grid.Row="3" Grid.Column="0" VerticalAlignment="Center" Margin="10,2,2,2">Value:</TextBlock>
<NumericUpDown Grid.Row="3" Grid.Column="1" Value="{Binding #upDown.Value}" VerticalAlignment="Center"
Margin="2" Width="70" HorizontalAlignment="Center"/>
Margin="2" HorizontalAlignment="Center"/>
</Grid>
</Grid>
@ -73,7 +73,7 @@
<StackPanel Margin="2,10,2,2" Orientation="Horizontal" Spacing="10">
<TextBlock FontSize="14" FontWeight="Bold" VerticalAlignment="Center">Usage of NumericUpDown:</TextBlock>
<NumericUpDown Name="upDown" Minimum="0" Maximum="10" Increment="0.5"
CultureInfo="en-US" VerticalAlignment="Center" Width="100"
CultureInfo="en-US" VerticalAlignment="Center"
Watermark="Enter text" FormatString="{Binding SelectedFormat.Value}"/>
</StackPanel>

2
samples/ControlCatalog/Pages/ToolTipPage.xaml

@ -18,7 +18,7 @@
ToolTip.Tip="This is a ToolTip">
<TextBlock>Hover Here</TextBlock>
</Border>
<CheckBox Grid.Column="1"
<ToggleSwitch Grid.Column="1"
Margin="5"
Grid.Row="0"
IsChecked="{Binding ElementName=Border, Path=(ToolTip.IsOpen)}"

3
samples/RenderDemo/MainWindow.xaml

@ -29,6 +29,9 @@
<TabItem Header="Animations">
<pages:AnimationsPage/>
</TabItem>
<TabItem Header="Transitions">
<pages:TransitionsPage/>
</TabItem>
<TabItem Header="Clipping">
<pages:ClippingPage/>
</TabItem>

115
samples/RenderDemo/Pages/TransitionsPage.xaml

@ -0,0 +1,115 @@
<UserControl
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="RenderDemo.Pages.TransitionsPage">
<UserControl.Styles>
<Styles>
<Styles.Resources>
<Template x:Key="Acorn">
<Path Fill="White" Stretch="Uniform"
Data="F1 M 16.6309,18.6563C 17.1309,
8.15625 29.8809,14.1563 29.8809,
14.1563C 30.8809,11.1563 34.1308,
11.4063 34.1308,11.4063C 33.5,12
34.6309,13.1563 34.6309,13.1563C
32.1309,13.1562 31.1309,14.9062
31.1309,14.9062C 41.1309,23.9062
32.6309,27.9063 32.6309,27.9062C
24.6309,24.9063 21.1309,22.1562
16.6309,18.6563 Z M 16.6309,19.9063C
21.6309,24.1563 25.1309,26.1562
31.6309,28.6562C 31.6309,28.6562
26.3809,39.1562 18.3809,36.1563C
18.3809,36.1563 18,38 16.3809,36.9063C
15,36 16.3809,34.9063 16.3809,34.9063C
16.3809,34.9063 10.1309,30.9062 16.6309,19.9063 Z"/>
</Template>
<Template x:Key="Heart">
<Path Fill="Red" Stretch="Uniform" Data="
M 272.70141,238.71731
C 206.46141,238.71731 152.70146,292.4773 152.70146,358.71731
C 152.70146,493.47282 288.63461,528.80461 381.26391,662.02535
C 468.83815,529.62199 609.82641,489.17075 609.82641,358.71731
C 609.82641,292.47731 556.06651,238.7173 489.82641,238.71731
C 441.77851,238.71731 400.42481,267.08774 381.26391,307.90481
C 362.10311,267.08773 320.74941,238.7173 272.70141,238.71731 z "/>
</Template>
</Styles.Resources>
<Style Selector="Border.Test">
<Setter Property="Margin" Value="15"/>
<Setter Property="Width" Value="100"/>
<Setter Property="Height" Value="100"/>
<Setter Property="Child" Value="{StaticResource Acorn}"/>
<Setter Property="Transitions">
<Transitions>
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:1" />
</Transitions>
</Setter>
<Setter Property="RenderTransform" Value="none" />
</Style>
<Style Selector="Border.Rect1:pointerover">
<Setter Property="RenderTransform" Value="rotate(120deg) scale(1.5)" />
</Style>
<Style Selector="Border.Rect2:pointerover">
<Setter Property="RenderTransform" Value="scale(0.8)" />
</Style>
<Style Selector="Border.Rect3">
<Setter Property="Child" Value="{StaticResource Heart}"/>
</Style>
<Style Selector="Border.Rect3:pointerover">
<Setter Property="RenderTransform" Value="rotate(1turn)" />
</Style>
<Style Selector="Border.Rect4:pointerover">
<Setter Property="RenderTransform" Value="translateY(-100px)" />
</Style>
<Style Selector="Border.Rect5:pointerover">
<Setter Property="RenderTransform" Value="skewX(-20deg)" />
</Style>
<Style Selector="Border.Rect5:pointerover">
<Setter Property="RenderTransform" Value="skewX(-20deg)" />
</Style>
<Style Selector="Border.Rect6">
<Setter Property="Transitions">
<Transitions>
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.5" Delay="0:0:1"/>
</Transitions>
</Setter>
<Setter Property="RenderTransform" Value="scale(0.8)" />
</Style>
<Style Selector="Border.Rect6:pointerover">
<Setter Property="RenderTransform" Value="none" />
</Style>
</Styles>
</UserControl.Styles>
<Grid>
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" ClipToBounds="False">
<StackPanel.Clock>
<Clock />
</StackPanel.Clock>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock VerticalAlignment="Center">Hover to activate Transform Keyframe Animations.</TextBlock>
<Button Content="{Binding PlayStateText}" Command="{Binding TogglePlayState}" Click="ToggleClock" />
</StackPanel>
<WrapPanel ClipToBounds="False">
<Border Classes="Test Rect1" Background="DarkRed"/>
<Border Classes="Test Rect2" Background="Magenta"/>
<Border Classes="Test Rect3"/>
<Border Classes="Test Rect4" Background="Navy"/>
<Border Classes="Test Rect5" Background="SeaGreen"/>
<Border Classes="Test Rect6" Background="Orange"/>
</WrapPanel>
</StackPanel>
</Grid>
</UserControl>

37
samples/RenderDemo/Pages/TransitionsPage.xaml.cs

@ -0,0 +1,37 @@
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using RenderDemo.ViewModels;
namespace RenderDemo.Pages
{
public class TransitionsPage : UserControl
{
public TransitionsPage()
{
InitializeComponent();
this.DataContext = new AnimationsPageViewModel();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private void ToggleClock(object sender, RoutedEventArgs args)
{
var button = sender as Button;
var clock = button.Clock;
if (clock.PlayState == PlayState.Run)
{
clock.PlayState = PlayState.Pause;
}
else if (clock.PlayState == PlayState.Pause)
{
clock.PlayState = PlayState.Run;
}
}
}
}

36
src/Avalonia.Animation/TransitionInstance.cs

@ -4,6 +4,7 @@ using System.Reactive.Linq;
using Avalonia.Animation.Easings;
using Avalonia.Animation.Utils;
using Avalonia.Reactive;
using Avalonia.Utilities;
namespace Avalonia.Animation
{
@ -13,31 +14,56 @@ namespace Avalonia.Animation
internal class TransitionInstance : SingleSubscriberObservableBase<double>
{
private IDisposable _timerSubscription;
private TimeSpan _delay;
private TimeSpan _duration;
private readonly IClock _baseClock;
private IClock _clock;
public TransitionInstance(IClock clock, TimeSpan Duration)
public TransitionInstance(IClock clock, TimeSpan delay, TimeSpan duration)
{
clock = clock ?? throw new ArgumentNullException(nameof(clock));
_duration = Duration;
_delay = delay;
_duration = duration;
_baseClock = clock;
}
private void TimerTick(TimeSpan t)
{
var interpVal = _duration.Ticks == 0 ? 1d : (double)t.Ticks / _duration.Ticks;
// [<------------- normalizedTotalDur ------------------>]
// [<---- Delay ---->][<---------- Duration ------------>]
// ^- normalizedDelayEnd
// [<---- normalizedInterpVal --->]
var normalizedInterpVal = 1d;
if (!MathUtilities.AreClose(_duration.TotalSeconds, 0d))
{
var normalizedTotalDur = _delay + _duration;
var normalizedDelayEnd = _delay.TotalSeconds / normalizedTotalDur.TotalSeconds;
var normalizedPresentationTime = t.TotalSeconds / normalizedTotalDur.TotalSeconds;
if (normalizedPresentationTime < normalizedDelayEnd
|| MathUtilities.AreClose(normalizedPresentationTime, normalizedDelayEnd))
{
normalizedInterpVal = 0d;
}
else
{
normalizedInterpVal = (t.TotalSeconds - _delay.TotalSeconds) / _duration.TotalSeconds;
}
}
// Clamp interpolation value.
if (interpVal >= 1d | interpVal < 0d)
if (normalizedInterpVal >= 1d || normalizedInterpVal < 0d)
{
PublishNext(1d);
PublishCompleted();
}
else
{
PublishNext(interpVal);
PublishNext(normalizedInterpVal);
}
}

9
src/Avalonia.Animation/Transition`1.cs

@ -13,10 +13,15 @@ namespace Avalonia.Animation
private AvaloniaProperty _prop;
/// <summary>
/// Gets the duration of the animation.
/// Gets or sets the duration of the transition.
/// </summary>
public TimeSpan Duration { get; set; }
/// <summary>
/// Gets or sets delay before starting the transition.
/// </summary>
public TimeSpan Delay { get; set; } = TimeSpan.Zero;
/// <summary>
/// Gets the easing class to be used.
/// </summary>
@ -47,7 +52,7 @@ namespace Avalonia.Animation
/// <inheritdocs/>
public virtual IDisposable Apply(Animatable control, IClock clock, object oldValue, object newValue)
{
var transition = DoTransition(new TransitionInstance(clock, Duration), (T)oldValue, (T)newValue);
var transition = DoTransition(new TransitionInstance(clock, Delay, Duration), (T)oldValue, (T)newValue);
return control.Bind<T>((AvaloniaProperty<T>)Property, transition, Data.BindingPriority.Animation);
}
}

1
src/Avalonia.Base/Properties/AssemblyInfo.cs

@ -8,3 +8,4 @@ using Avalonia.Metadata;
[assembly: InternalsVisibleTo("Avalonia.Base.UnitTests")]
[assembly: InternalsVisibleTo("Avalonia.UnitTests")]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid")]

47
src/Avalonia.Base/Utilities/MathUtilities.cs

@ -1,5 +1,4 @@
using System;
using System.Runtime.InteropServices;
namespace Avalonia.Utilities
{
@ -9,7 +8,7 @@ namespace Avalonia.Utilities
public static class MathUtilities
{
// smallest such that 1.0+DoubleEpsilon != 1.0
private const double DoubleEpsilon = 2.2204460492503131e-016;
internal static readonly double DoubleEpsilon = 2.2204460492503131e-016;
private const float FloatEpsilon = 1.192092896e-07F;
@ -188,6 +187,11 @@ namespace Avalonia.Utilities
/// <returns>The clamped value.</returns>
public static double Clamp(double val, double min, double max)
{
if (min > max)
{
ThrowCannotBeGreaterThanException(min, max);
}
if (val < min)
{
return min;
@ -216,7 +220,7 @@ namespace Avalonia.Utilities
double newValue;
// If DPI == 1, don't use DPI-aware rounding.
if (!MathUtilities.AreClose(dpiScale, 1.0))
if (!MathUtilities.IsOne(dpiScale))
{
newValue = Math.Round(value * dpiScale) / dpiScale;
// If rounding produces a value unacceptable to layout (NaN, Infinity or MaxValue), use the original value.
@ -246,7 +250,7 @@ namespace Avalonia.Utilities
{
if (min > max)
{
throw new ArgumentException($"{min} cannot be greater than {max}.");
ThrowCannotBeGreaterThanException(min, max);
}
if (val < min)
@ -262,5 +266,40 @@ namespace Avalonia.Utilities
return val;
}
}
/// <summary>
/// Converts an angle in degrees to radians.
/// </summary>
/// <param name="angle">The angle in degrees.</param>
/// <returns>The angle in radians.</returns>
public static double Deg2Rad(double angle)
{
return angle * (Math.PI / 180d);
}
/// <summary>
/// Converts an angle in gradians to radians.
/// </summary>
/// <param name="angle">The angle in gradians.</param>
/// <returns>The angle in radians.</returns>
public static double Grad2Rad(double angle)
{
return angle * (Math.PI / 200d);
}
/// <summary>
/// Converts an angle in turns to radians.
/// </summary>
/// <param name="angle">The angle in turns.</param>
/// <returns>The angle in radians.</returns>
public static double Turn2Rad(double angle)
{
return angle * 2 * Math.PI;
}
private static void ThrowCannotBeGreaterThanException(double min, double max)
{
throw new ArgumentException($"{min} cannot be greater than {max}.");
}
}
}

38
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -2681,7 +2681,7 @@ namespace Avalonia.Controls
{
return;
}
Debug.Assert(DoubleUtil.LessThanOrClose(_vScrollBar.Value, _vScrollBar.Maximum));
Debug.Assert(MathUtilities.LessThanOrClose(_vScrollBar.Value, _vScrollBar.Maximum));
_verticalScrollChangesIgnored++;
try
@ -2698,7 +2698,7 @@ namespace Avalonia.Controls
}
else if (scrollEventType == ScrollEventType.SmallDecrement)
{
if (DoubleUtil.GreaterThan(NegVerticalOffset, 0))
if (MathUtilities.GreaterThan(NegVerticalOffset, 0))
{
DisplayData.PendingVerticalScrollHeight -= NegVerticalOffset;
}
@ -2717,7 +2717,7 @@ namespace Avalonia.Controls
DisplayData.PendingVerticalScrollHeight = _vScrollBar.Value - _verticalOffset;
}
if (!DoubleUtil.IsZero(DisplayData.PendingVerticalScrollHeight))
if (!MathUtilities.IsZero(DisplayData.PendingVerticalScrollHeight))
{
// Invalidate so the scroll happens on idle
InvalidateRowsMeasure(invalidateIndividualElements: false);
@ -3346,22 +3346,22 @@ namespace Avalonia.Controls
bool needHorizScrollbarWithoutVertScrollbar = false;
if (allowHorizScrollbar &&
DoubleUtil.GreaterThan(totalVisibleWidth, cellsWidth) &&
DoubleUtil.LessThan(totalVisibleFrozenWidth, cellsWidth) &&
DoubleUtil.LessThanOrClose(horizScrollBarHeight, cellsHeight))
MathUtilities.GreaterThan(totalVisibleWidth, cellsWidth) &&
MathUtilities.LessThan(totalVisibleFrozenWidth, cellsWidth) &&
MathUtilities.LessThanOrClose(horizScrollBarHeight, cellsHeight))
{
double oldDataHeight = cellsHeight;
cellsHeight -= horizScrollBarHeight;
Debug.Assert(cellsHeight >= 0);
needHorizScrollbarWithoutVertScrollbar = needHorizScrollbar = true;
if (allowVertScrollbar && (DoubleUtil.LessThanOrClose(totalVisibleWidth - cellsWidth, vertScrollBarWidth) ||
DoubleUtil.LessThanOrClose(cellsWidth - totalVisibleFrozenWidth, vertScrollBarWidth)))
if (allowVertScrollbar && (MathUtilities.LessThanOrClose(totalVisibleWidth - cellsWidth, vertScrollBarWidth) ||
MathUtilities.LessThanOrClose(cellsWidth - totalVisibleFrozenWidth, vertScrollBarWidth)))
{
// Would we still need a horizontal scrollbar without the vertical one?
UpdateDisplayedRows(DisplayData.FirstScrollingSlot, cellsHeight);
if (DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount)
{
needHorizScrollbar = DoubleUtil.LessThan(totalVisibleFrozenWidth, cellsWidth - vertScrollBarWidth);
needHorizScrollbar = MathUtilities.LessThan(totalVisibleFrozenWidth, cellsWidth - vertScrollBarWidth);
}
}
@ -3374,8 +3374,8 @@ namespace Avalonia.Controls
UpdateDisplayedRows(DisplayData.FirstScrollingSlot, cellsHeight);
if (allowVertScrollbar &&
DoubleUtil.GreaterThan(cellsHeight, 0) &&
DoubleUtil.LessThanOrClose(vertScrollBarWidth, cellsWidth) &&
MathUtilities.GreaterThan(cellsHeight, 0) &&
MathUtilities.LessThanOrClose(vertScrollBarWidth, cellsWidth) &&
DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount)
{
cellsWidth -= vertScrollBarWidth;
@ -3389,9 +3389,9 @@ namespace Avalonia.Controls
if (allowHorizScrollbar &&
needVertScrollbar && !needHorizScrollbar &&
DoubleUtil.GreaterThan(totalVisibleWidth, cellsWidth) &&
DoubleUtil.LessThan(totalVisibleFrozenWidth, cellsWidth) &&
DoubleUtil.LessThanOrClose(horizScrollBarHeight, cellsHeight))
MathUtilities.GreaterThan(totalVisibleWidth, cellsWidth) &&
MathUtilities.LessThan(totalVisibleFrozenWidth, cellsWidth) &&
MathUtilities.LessThanOrClose(horizScrollBarHeight, cellsHeight))
{
cellsWidth += vertScrollBarWidth;
cellsHeight -= horizScrollBarHeight;
@ -3422,7 +3422,7 @@ namespace Avalonia.Controls
if (allowVertScrollbar)
{
if (cellsHeight > 0 &&
DoubleUtil.LessThanOrClose(vertScrollBarWidth, cellsWidth) &&
MathUtilities.LessThanOrClose(vertScrollBarWidth, cellsWidth) &&
DisplayData.NumTotallyDisplayedScrollingElements != VisibleSlotCount)
{
cellsWidth -= vertScrollBarWidth;
@ -3439,9 +3439,9 @@ namespace Avalonia.Controls
if (allowHorizScrollbar)
{
if (cellsWidth > 0 &&
DoubleUtil.LessThanOrClose(horizScrollBarHeight, cellsHeight) &&
DoubleUtil.GreaterThan(totalVisibleWidth, cellsWidth) &&
DoubleUtil.LessThan(totalVisibleFrozenWidth, cellsWidth))
MathUtilities.LessThanOrClose(horizScrollBarHeight, cellsHeight) &&
MathUtilities.GreaterThan(totalVisibleWidth, cellsWidth) &&
MathUtilities.LessThan(totalVisibleFrozenWidth, cellsWidth))
{
cellsHeight -= horizScrollBarHeight;
Debug.Assert(cellsHeight >= 0);
@ -5387,7 +5387,7 @@ namespace Avalonia.Controls
private void SetVerticalOffset(double newVerticalOffset)
{
_verticalOffset = newVerticalOffset;
if (_vScrollBar != null && !DoubleUtil.AreClose(newVerticalOffset, _vScrollBar.Value))
if (_vScrollBar != null && !MathUtilities.AreClose(newVerticalOffset, _vScrollBar.Value))
{
_vScrollBar.Value = _verticalOffset;
}

2
src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs

@ -301,7 +301,7 @@ namespace Avalonia.Controls
private static bool CanResizeColumn(DataGridColumn column)
{
if (column.OwningGrid != null && column.OwningGrid.ColumnsInternal != null && column.OwningGrid.UsesStarSizing &&
(column.OwningGrid.ColumnsInternal.LastVisibleColumn == column || !DoubleUtil.AreClose(column.OwningGrid.ColumnsInternal.VisibleEdgedColumnsWidth, column.OwningGrid.CellsWidth)))
(column.OwningGrid.ColumnsInternal.LastVisibleColumn == column || !MathUtilities.AreClose(column.OwningGrid.ColumnsInternal.VisibleEdgedColumnsWidth, column.OwningGrid.CellsWidth)))
{
return false;
}

16
src/Avalonia.Controls.DataGrid/DataGridColumns.cs

@ -44,7 +44,7 @@ namespace Avalonia.Controls
/// <returns>The remaining amount of adjustment.</returns>
internal double AdjustColumnWidths(int displayIndex, double amount, bool userInitiated)
{
if (!DoubleUtil.IsZero(amount))
if (!MathUtilities.IsZero(amount))
{
if (amount < 0)
{
@ -777,7 +777,7 @@ namespace Avalonia.Controls
private double AdjustStarColumnWidths(int displayIndex, double adjustment, bool userInitiated)
{
double remainingAdjustment = adjustment;
if (DoubleUtil.IsZero(remainingAdjustment))
if (MathUtilities.IsZero(remainingAdjustment))
{
return remainingAdjustment;
}
@ -843,7 +843,7 @@ namespace Avalonia.Controls
/// <returns>The remaining amount of adjustment.</returns>
private double AdjustStarColumnWidths(int displayIndex, double remainingAdjustment, bool userInitiated, Func<DataGridColumn, double> targetWidth)
{
if (DoubleUtil.IsZero(remainingAdjustment))
if (MathUtilities.IsZero(remainingAdjustment))
{
return remainingAdjustment;
}
@ -1244,7 +1244,7 @@ namespace Avalonia.Controls
Debug.Assert(amount < 0);
Debug.Assert(column.Width.UnitType != DataGridLengthUnitType.Star);
if (DoubleUtil.GreaterThanOrClose(targetWidth, column.Width.DisplayValue))
if (MathUtilities.GreaterThanOrClose(targetWidth, column.Width.DisplayValue))
{
return amount;
}
@ -1271,7 +1271,7 @@ namespace Avalonia.Controls
/// <returns>The remaining amount of adjustment.</returns>
private double DecreaseNonStarColumnWidths(int displayIndex, Func<DataGridColumn, double> targetWidth, double amount, bool reverse, bool affectNewColumns)
{
if (DoubleUtil.GreaterThanOrClose(amount, 0))
if (MathUtilities.GreaterThanOrClose(amount, 0))
{
return amount;
}
@ -1285,7 +1285,7 @@ namespace Avalonia.Controls
(affectNewColumns || column.IsInitialDesiredWidthDetermined)))
{
amount = DecreaseNonStarColumnWidth(column, Math.Max(column.ActualMinWidth, targetWidth(column)), amount);
if (DoubleUtil.IsZero(amount))
if (MathUtilities.IsZero(amount))
{
break;
}
@ -1392,7 +1392,7 @@ namespace Avalonia.Controls
/// <returns>The remaining amount of adjustment.</returns>
private double IncreaseNonStarColumnWidths(int displayIndex, Func<DataGridColumn, double> targetWidth, double amount, bool reverse, bool affectNewColumns)
{
if (DoubleUtil.LessThanOrClose(amount, 0))
if (MathUtilities.LessThanOrClose(amount, 0))
{
return amount;
}
@ -1406,7 +1406,7 @@ namespace Avalonia.Controls
(affectNewColumns || column.IsInitialDesiredWidthDetermined)))
{
amount = IncreaseNonStarColumnWidth(column, Math.Min(column.ActualMaxWidth, targetWidth(column)), amount);
if (DoubleUtil.IsZero(amount))
if (MathUtilities.IsZero(amount))
{
break;
}

2
src/Avalonia.Controls.DataGrid/DataGridLength.cs

@ -529,7 +529,7 @@ namespace Avalonia.Controls
// in this case drop value part and print only "Star"
case DataGridLengthUnitType.Star:
return (
DoubleUtil.AreClose(1.0, dataGridLength.Value.Value)
MathUtilities.AreClose(1.0, dataGridLength.Value.Value)
? _starSuffix
: Convert.ToString(dataGridLength.Value.Value, culture ?? CultureInfo.CurrentCulture) + DataGridLengthConverter._starSuffix);

2
src/Avalonia.Controls.DataGrid/DataGridRow.cs

@ -879,7 +879,7 @@ namespace Avalonia.Controls
&& (double.IsNaN(_detailsContent.Height))
&& (AreDetailsVisible)
&& (!double.IsNaN(_detailsDesiredHeight))
&& !DoubleUtil.AreClose(_detailsContent.Bounds.Inflate(_detailsContent.Margin).Height, _detailsDesiredHeight)
&& !MathUtilities.AreClose(_detailsContent.Bounds.Inflate(_detailsContent.Margin).Height, _detailsDesiredHeight)
&& Slot != -1)
{
_detailsDesiredHeight = _detailsContent.Bounds.Inflate(_detailsContent.Margin).Height;

60
src/Avalonia.Controls.DataGrid/DataGridRows.cs

@ -329,7 +329,7 @@ namespace Avalonia.Controls
internal void OnRowsMeasure()
{
if (!DoubleUtil.IsZero(DisplayData.PendingVerticalScrollHeight))
if (!MathUtilities.IsZero(DisplayData.PendingVerticalScrollHeight))
{
ScrollSlotsByHeight(DisplayData.PendingVerticalScrollHeight);
DisplayData.PendingVerticalScrollHeight = 0;
@ -432,7 +432,7 @@ namespace Avalonia.Controls
}
else if (DisplayData.FirstScrollingSlot == slot && slot != -1)
{
if (!DoubleUtil.IsZero(NegVerticalOffset))
if (!MathUtilities.IsZero(NegVerticalOffset))
{
// First displayed row is partially scrolled of. Let's scroll it so that NegVerticalOffset becomes 0.
DisplayData.PendingVerticalScrollHeight = -NegVerticalOffset;
@ -447,7 +447,7 @@ namespace Avalonia.Controls
{
// Scroll up to the new row so it becomes the first displayed row
firstFullSlot = DisplayData.FirstScrollingSlot - 1;
if (DoubleUtil.GreaterThan(NegVerticalOffset, 0))
if (MathUtilities.GreaterThan(NegVerticalOffset, 0))
{
deltaY = -NegVerticalOffset;
}
@ -470,7 +470,7 @@ namespace Avalonia.Controls
// Figure out how much of the last row is cut off
double rowHeight = GetExactSlotElementHeight(DisplayData.LastScrollingSlot);
double availableHeight = AvailableSlotElementRoom + rowHeight;
if (DoubleUtil.AreClose(rowHeight, availableHeight))
if (MathUtilities.AreClose(rowHeight, availableHeight))
{
if (DisplayData.LastScrollingSlot == slot)
{
@ -499,7 +499,7 @@ namespace Avalonia.Controls
{
ResetDisplayedRows();
}
if (DoubleUtil.GreaterThanOrClose(GetExactSlotElementHeight(slot), CellsHeight))
if (MathUtilities.GreaterThanOrClose(GetExactSlotElementHeight(slot), CellsHeight))
{
// The entire row won't fit in the DataGrid so we start showing it from the top
NegVerticalOffset = 0;
@ -519,7 +519,7 @@ namespace Avalonia.Controls
}
//
Debug.Assert(DoubleUtil.LessThanOrClose(NegVerticalOffset, _verticalOffset));
Debug.Assert(MathUtilities.LessThanOrClose(NegVerticalOffset, _verticalOffset));
SetVerticalOffset(_verticalOffset);
@ -1660,7 +1660,7 @@ namespace Avalonia.Controls
private void ScrollSlotsByHeight(double height)
{
Debug.Assert(DisplayData.FirstScrollingSlot >= 0);
Debug.Assert(!DoubleUtil.IsZero(height));
Debug.Assert(!MathUtilities.IsZero(height));
_scrollingByHeight = true;
try
@ -1672,7 +1672,7 @@ namespace Avalonia.Controls
{
// Scrolling Down
int lastVisibleSlot = GetPreviousVisibleSlot(SlotCount);
if (_vScrollBar != null && DoubleUtil.AreClose(_vScrollBar.Maximum, newVerticalOffset))
if (_vScrollBar != null && MathUtilities.AreClose(_vScrollBar.Maximum, newVerticalOffset))
{
// We've scrolled to the bottom of the ScrollBar, automatically place the user at the very bottom
// of the DataGrid. If this produces very odd behavior, evaluate the coping strategy used by
@ -1684,7 +1684,7 @@ namespace Avalonia.Controls
else
{
deltaY = GetSlotElementHeight(newFirstScrollingSlot) - NegVerticalOffset;
if (DoubleUtil.LessThan(height, deltaY))
if (MathUtilities.LessThan(height, deltaY))
{
// We've merely covered up more of the same row we're on
NegVerticalOffset += height;
@ -1707,7 +1707,7 @@ namespace Avalonia.Controls
}
else
{
while (DoubleUtil.LessThanOrClose(deltaY, height))
while (MathUtilities.LessThanOrClose(deltaY, height))
{
if (newFirstScrollingSlot < lastVisibleSlot)
{
@ -1727,7 +1727,7 @@ namespace Avalonia.Controls
double rowHeight = GetExactSlotElementHeight(newFirstScrollingSlot);
double remainingHeight = height - deltaY;
if (DoubleUtil.LessThanOrClose(rowHeight, remainingHeight))
if (MathUtilities.LessThanOrClose(rowHeight, remainingHeight))
{
deltaY += rowHeight;
}
@ -1744,7 +1744,7 @@ namespace Avalonia.Controls
else
{
// Scrolling Up
if (DoubleUtil.GreaterThanOrClose(height + NegVerticalOffset, 0))
if (MathUtilities.GreaterThanOrClose(height + NegVerticalOffset, 0))
{
// We've merely exposing more of the row we're on
NegVerticalOffset += height;
@ -1778,7 +1778,7 @@ namespace Avalonia.Controls
else
{
int lastScrollingSlot = DisplayData.LastScrollingSlot;
while (DoubleUtil.GreaterThan(deltaY, height))
while (MathUtilities.GreaterThan(deltaY, height))
{
if (newFirstScrollingSlot > 0)
{
@ -1797,7 +1797,7 @@ namespace Avalonia.Controls
}
double rowHeight = GetExactSlotElementHeight(newFirstScrollingSlot);
double remainingHeight = height - deltaY;
if (DoubleUtil.LessThanOrClose(rowHeight + remainingHeight, 0))
if (MathUtilities.LessThanOrClose(rowHeight + remainingHeight, 0))
{
deltaY -= rowHeight;
}
@ -1809,7 +1809,7 @@ namespace Avalonia.Controls
}
}
}
if (DoubleUtil.GreaterThanOrClose(0, newVerticalOffset) && newFirstScrollingSlot != 0)
if (MathUtilities.GreaterThanOrClose(0, newVerticalOffset) && newFirstScrollingSlot != 0)
{
// We've scrolled to the top of the ScrollBar, automatically place the user at the very top
// of the DataGrid. If this produces very odd behavior, evaluate the RowHeight estimate.
@ -1822,7 +1822,7 @@ namespace Avalonia.Controls
}
double firstRowHeight = GetExactSlotElementHeight(newFirstScrollingSlot);
if (DoubleUtil.LessThan(firstRowHeight, NegVerticalOffset))
if (MathUtilities.LessThan(firstRowHeight, NegVerticalOffset))
{
// We've scrolled off more of the first row than what's possible. This can happen
// if the first row got shorter (Ex: Collpasing RowDetails) or if the user has a recycling
@ -1838,11 +1838,11 @@ namespace Avalonia.Controls
UpdateDisplayedRows(newFirstScrollingSlot, CellsHeight);
double firstElementHeight = GetExactSlotElementHeight(DisplayData.FirstScrollingSlot);
if (DoubleUtil.GreaterThan(NegVerticalOffset, firstElementHeight))
if (MathUtilities.GreaterThan(NegVerticalOffset, firstElementHeight))
{
int firstElementSlot = DisplayData.FirstScrollingSlot;
// We filled in some rows at the top and now we have a NegVerticalOffset that's greater than the first element
while (newFirstScrollingSlot > 0 && DoubleUtil.GreaterThan(NegVerticalOffset, firstElementHeight))
while (newFirstScrollingSlot > 0 && MathUtilities.GreaterThan(NegVerticalOffset, firstElementHeight))
{
int previousSlot = GetPreviousVisibleSlot(firstElementSlot);
if (previousSlot == -1)
@ -1872,7 +1872,7 @@ namespace Avalonia.Controls
{
_verticalOffset = NegVerticalOffset;
}
else if (DoubleUtil.GreaterThan(NegVerticalOffset, newVerticalOffset))
else if (MathUtilities.GreaterThan(NegVerticalOffset, newVerticalOffset))
{
// The scrolled-in row was larger than anticipated. Adjust the DataGrid so the ScrollBar thumb
// can stay in the same place
@ -1890,8 +1890,8 @@ namespace Avalonia.Controls
DisplayData.FullyRecycleElements();
Debug.Assert(DoubleUtil.GreaterThanOrClose(NegVerticalOffset, 0));
Debug.Assert(DoubleUtil.GreaterThanOrClose(_verticalOffset, NegVerticalOffset));
Debug.Assert(MathUtilities.GreaterThanOrClose(NegVerticalOffset, 0));
Debug.Assert(MathUtilities.GreaterThanOrClose(_verticalOffset, NegVerticalOffset));
}
finally
{
@ -2032,7 +2032,7 @@ namespace Avalonia.Controls
double deltaY = -NegVerticalOffset;
int visibleScrollingRows = 0;
if (DoubleUtil.LessThanOrClose(displayHeight, 0) || SlotCount == 0 || ColumnsItemsInternal.Count == 0)
if (MathUtilities.LessThanOrClose(displayHeight, 0) || SlotCount == 0 || ColumnsItemsInternal.Count == 0)
{
return;
}
@ -2044,7 +2044,7 @@ namespace Avalonia.Controls
}
int slot = firstDisplayedScrollingSlot;
while (slot < SlotCount && !DoubleUtil.GreaterThanOrClose(deltaY, displayHeight))
while (slot < SlotCount && !MathUtilities.GreaterThanOrClose(deltaY, displayHeight))
{
deltaY += GetExactSlotElementHeight(slot);
visibleScrollingRows++;
@ -2052,7 +2052,7 @@ namespace Avalonia.Controls
slot = GetNextVisibleSlot(slot);
}
while (DoubleUtil.LessThan(deltaY, displayHeight) && slot >= 0)
while (MathUtilities.LessThan(deltaY, displayHeight) && slot >= 0)
{
slot = GetPreviousVisibleSlot(firstDisplayedScrollingSlot);
if (slot >= 0)
@ -2063,14 +2063,14 @@ namespace Avalonia.Controls
}
}
// If we're up to the first row, and we still have room left, uncover as much of the first row as we can
if (firstDisplayedScrollingSlot == 0 && DoubleUtil.LessThan(deltaY, displayHeight))
if (firstDisplayedScrollingSlot == 0 && MathUtilities.LessThan(deltaY, displayHeight))
{
double newNegVerticalOffset = Math.Max(0, NegVerticalOffset - displayHeight + deltaY);
deltaY += NegVerticalOffset - newNegVerticalOffset;
NegVerticalOffset = newNegVerticalOffset;
}
if (DoubleUtil.GreaterThan(deltaY, displayHeight) || (DoubleUtil.AreClose(deltaY, displayHeight) && DoubleUtil.GreaterThan(NegVerticalOffset, 0)))
if (MathUtilities.GreaterThan(deltaY, displayHeight) || (MathUtilities.AreClose(deltaY, displayHeight) && MathUtilities.GreaterThan(NegVerticalOffset, 0)))
{
DisplayData.NumTotallyDisplayedScrollingElements = visibleScrollingRows - 1;
}
@ -2108,7 +2108,7 @@ namespace Avalonia.Controls
double deltaY = 0;
int visibleScrollingRows = 0;
if (DoubleUtil.LessThanOrClose(displayHeight, 0) || SlotCount == 0 || ColumnsItemsInternal.Count == 0)
if (MathUtilities.LessThanOrClose(displayHeight, 0) || SlotCount == 0 || ColumnsItemsInternal.Count == 0)
{
ResetDisplayedRows();
return;
@ -2120,7 +2120,7 @@ namespace Avalonia.Controls
}
int slot = lastDisplayedScrollingRow;
while (DoubleUtil.LessThan(deltaY, displayHeight) && slot >= 0)
while (MathUtilities.LessThan(deltaY, displayHeight) && slot >= 0)
{
deltaY += GetExactSlotElementHeight(slot);
visibleScrollingRows++;
@ -2542,7 +2542,7 @@ namespace Avalonia.Controls
double heightChange = UpdateRowGroupVisibility(rowGroupInfo, isVisible, isDisplayed: false);
// Use epsilon instead of 0 here so that in the off chance that our estimates put the vertical offset negative
// the user can still scroll to the top since the offset is non-zero
SetVerticalOffset(Math.Max(DoubleUtil.DBL_EPSILON, _verticalOffset + heightChange));
SetVerticalOffset(Math.Max(MathUtilities.DoubleEpsilon, _verticalOffset + heightChange));
}
else
{
@ -3024,4 +3024,4 @@ namespace Avalonia.Controls
}
#endif
}
}
}

6
src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs

@ -310,9 +310,9 @@ namespace Avalonia.Controls.Primitives
double leftEdge = column.IsFrozen ? frozenLeftEdge : scrollingLeftEdge;
double rightEdge = leftEdge + column.ActualWidth;
return
DoubleUtil.GreaterThan(rightEdge, 0) &&
DoubleUtil.LessThanOrClose(leftEdge, OwningGrid.CellsWidth) &&
DoubleUtil.GreaterThan(rightEdge, frozenLeftEdge); // scrolling column covered up by frozen column(s)
MathUtilities.GreaterThan(rightEdge, 0) &&
MathUtilities.LessThanOrClose(leftEdge, OwningGrid.CellsWidth) &&
MathUtilities.GreaterThan(rightEdge, frozenLeftEdge); // scrolling column covered up by frozen column(s)
}
}
}

136
src/Avalonia.Controls.DataGrid/Utils/DoubleUtil.cs

@ -1,136 +0,0 @@
// (c) Copyright Microsoft Corporation.
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.
// All other rights reserved.
using System;
namespace Avalonia.Controls.Utils
{
internal static class DoubleUtil
{
internal const double DBL_EPSILON = 1e-6;
/// <summary>
/// AreClose - Returns whether or not two doubles are "close". That is, whether or
/// not they are within epsilon of each other. Note that this epsilon is proportional
/// to the numbers themselves to that AreClose survives scalar multiplication.
/// There are plenty of ways for this to return false even for numbers which
/// are theoretically identical, so no code calling this should fail to work if this
/// returns false. This is important enough to repeat:
/// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be
/// used for optimizations *only*.
/// </summary>
/// <returns>
/// bool - the result of the AreClose comparison.
/// </returns>
/// <param name="value1"> The first double to compare. </param>
/// <param name="value2"> The second double to compare. </param>
public static bool AreClose(double value1, double value2)
{
//in case they are Infinities (then epsilon check does not work)
if (value1 == value2) return true;
// This computes (|value1-value2| / (|value1| + |value2| + 10.0)) < DBL_EPSILON
double eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * DBL_EPSILON;
double delta = value1 - value2;
return (-eps < delta) && (eps > delta);
}
/// <summary>
/// GreaterThan - Returns whether or not the first double is greater than the second double.
/// That is, whether or not the first is strictly greater than *and* not within epsilon of
/// the other number. Note that this epsilon is proportional to the numbers themselves
/// to that AreClose survives scalar multiplication. Note,
/// There are plenty of ways for this to return false even for numbers which
/// are theoretically identical, so no code calling this should fail to work if this
/// returns false. This is important enough to repeat:
/// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be
/// used for optimizations *only*.
/// </summary>
/// <returns>
/// bool - the result of the GreaterThan comparison.
/// </returns>
/// <param name="value1"> The first double to compare. </param>
/// <param name="value2"> The second double to compare. </param>
public static bool GreaterThan(double value1, double value2)
{
return (value1 > value2) && !AreClose(value1, value2);
}
/// <summary>
/// GreaterThanOrClose - Returns whether or not the first double is greater than or close to
/// the second double. That is, whether or not the first is strictly greater than or within
/// epsilon of the other number. Note that this epsilon is proportional to the numbers
/// themselves to that AreClose survives scalar multiplication. Note,
/// There are plenty of ways for this to return false even for numbers which
/// are theoretically identical, so no code calling this should fail to work if this
/// returns false. This is important enough to repeat:
/// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be
/// used for optimizations *only*.
/// </summary>
/// <returns>
/// bool - the result of the GreaterThanOrClose comparison.
/// </returns>
/// <param name="value1"> The first double to compare. </param>
/// <param name="value2"> The second double to compare. </param>
public static bool GreaterThanOrClose(double value1, double value2)
{
return (value1 > value2) || AreClose(value1, value2);
}
/// <summary>
/// IsZero - Returns whether or not the double is "close" to 0. Same as AreClose(double, 0),
/// but this is faster.
/// </summary>
/// <returns>
/// bool - the result of the IsZero comparison.
/// </returns>
/// <param name="value"> The double to compare to 0. </param>
public static bool IsZero(double value)
{
return Math.Abs(value) < 10.0 * DBL_EPSILON;
}
/// <summary>
/// LessThan - Returns whether or not the first double is less than the second double.
/// That is, whether or not the first is strictly less than *and* not within epsilon of
/// the other number. Note that this epsilon is proportional to the numbers themselves
/// to that AreClose survives scalar multiplication. Note,
/// There are plenty of ways for this to return false even for numbers which
/// are theoretically identical, so no code calling this should fail to work if this
/// returns false. This is important enough to repeat:
/// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be
/// used for optimizations *only*.
/// </summary>
/// <returns>
/// bool - the result of the LessThan comparison.
/// </returns>
/// <param name="value1"> The first double to compare. </param>
/// <param name="value2"> The second double to compare. </param>
public static bool LessThan(double value1, double value2)
{
return (value1 < value2) && !AreClose(value1, value2);
}
/// <summary>
/// LessThanOrClose - Returns whether or not the first double is less than or close to
/// the second double. That is, whether or not the first is strictly less than or within
/// epsilon of the other number. Note that this epsilon is proportional to the numbers
/// themselves to that AreClose survives scalar multiplication. Note,
/// There are plenty of ways for this to return false even for numbers which
/// are theoretically identical, so no code calling this should fail to work if this
/// returns false. This is important enough to repeat:
/// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be
/// used for optimizations *only*.
/// </summary>
/// <returns>
/// bool - the result of the LessThanOrClose comparison.
/// </returns>
/// <param name="value1"> The first double to compare. </param>
/// <param name="value2"> The second double to compare. </param>
public static bool LessThanOrClose(double value1, double value2)
{
return (value1 < value2) || AreClose(value1, value2);
}
}
}

167
src/Avalonia.Controls/ContextMenu.cs

@ -1,17 +1,18 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Styling;
#nullable enable
namespace Avalonia.Controls
{
/// <summary>
@ -19,11 +20,59 @@ namespace Avalonia.Controls
/// </summary>
public class ContextMenu : MenuBase, ISetterValue
{
/// <summary>
/// Defines the <see cref="HorizontalOffset"/> property.
/// </summary>
public static readonly StyledProperty<double> HorizontalOffsetProperty =
Popup.HorizontalOffsetProperty.AddOwner<ContextMenu>();
/// <summary>
/// Defines the <see cref="VerticalOffset"/> property.
/// </summary>
public static readonly StyledProperty<double> VerticalOffsetProperty =
Popup.VerticalOffsetProperty.AddOwner<ContextMenu>();
/// <summary>
/// Defines the <see cref="PlacementAnchor"/> property.
/// </summary>
public static readonly StyledProperty<PopupAnchor> PlacementAnchorProperty =
Popup.PlacementAnchorProperty.AddOwner<ContextMenu>();
/// <summary>
/// Defines the <see cref="PlacementConstraintAdjustment"/> property.
/// </summary>
public static readonly StyledProperty<PopupPositionerConstraintAdjustment> PlacementConstraintAdjustmentProperty =
Popup.PlacementConstraintAdjustmentProperty.AddOwner<ContextMenu>();
/// <summary>
/// Defines the <see cref="PlacementGravity"/> property.
/// </summary>
public static readonly StyledProperty<PopupGravity> PlacementGravityProperty =
Popup.PlacementGravityProperty.AddOwner<ContextMenu>();
/// <summary>
/// Defines the <see cref="PlacementMode"/> property.
/// </summary>
public static readonly StyledProperty<PlacementMode> PlacementModeProperty =
Popup.PlacementModeProperty.AddOwner<ContextMenu>();
/// <summary>
/// Defines the <see cref="PlacementRect"/> property.
/// </summary>
public static readonly StyledProperty<Rect?> PlacementRectProperty =
AvaloniaProperty.Register<Popup, Rect?>(nameof(PlacementRect));
/// <summary>
/// Defines the <see cref="PlacementTarget"/> property.
/// </summary>
public static readonly StyledProperty<Control?> PlacementTargetProperty =
Popup.PlacementTargetProperty.AddOwner<ContextMenu>();
private static readonly ITemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => new StackPanel { Orientation = Orientation.Vertical });
private Popup _popup;
private List<Control> _attachedControls;
private IInputElement _previousFocus;
private Popup? _popup;
private List<Control>? _attachedControls;
private IInputElement? _previousFocus;
/// <summary>
/// Initializes a new instance of the <see cref="ContextMenu"/> class.
@ -47,23 +96,107 @@ namespace Avalonia.Controls
/// </summary>
static ContextMenu()
{
ItemsPanelProperty.OverrideDefaultValue(typeof(ContextMenu), DefaultPanel);
ItemsPanelProperty.OverrideDefaultValue<ContextMenu>(DefaultPanel);
PlacementModeProperty.OverrideDefaultValue<ContextMenu>(PlacementMode.Pointer);
ContextMenuProperty.Changed.Subscribe(ContextMenuChanged);
}
/// <summary>
/// Gets or sets the Horizontal offset of the context menu in relation to the <see cref="PlacementTarget"/>.
/// </summary>
public double HorizontalOffset
{
get { return GetValue(HorizontalOffsetProperty); }
set { SetValue(HorizontalOffsetProperty, value); }
}
/// <summary>
/// Gets or sets the Vertical offset of the context menu in relation to the <see cref="PlacementTarget"/>.
/// </summary>
public double VerticalOffset
{
get { return GetValue(VerticalOffsetProperty); }
set { SetValue(VerticalOffsetProperty, value); }
}
/// <summary>
/// Gets or sets the anchor point on the <see cref="PlacementRect"/> when <see cref="PlacementMode"/>
/// is <see cref="PlacementMode.AnchorAndGravity"/>.
/// </summary>
public PopupAnchor PlacementAnchor
{
get { return GetValue(PlacementAnchorProperty); }
set { SetValue(PlacementAnchorProperty, value); }
}
/// <summary>
/// Gets or sets a value describing how the context menu position will be adjusted if the
/// unadjusted position would result in the context menu being partly constrained.
/// </summary>
public PopupPositionerConstraintAdjustment PlacementConstraintAdjustment
{
get { return GetValue(PlacementConstraintAdjustmentProperty); }
set { SetValue(PlacementConstraintAdjustmentProperty, value); }
}
/// <summary>
/// Gets or sets a value which defines in what direction the context menu should open
/// when <see cref="PlacementMode"/> is <see cref="PlacementMode.AnchorAndGravity"/>.
/// </summary>
public PopupGravity PlacementGravity
{
get { return GetValue(PlacementGravityProperty); }
set { SetValue(PlacementGravityProperty, value); }
}
/// <summary>
/// Gets or sets the placement mode of the context menu in relation to the<see cref="PlacementTarget"/>.
/// </summary>
public PlacementMode PlacementMode
{
get { return GetValue(PlacementModeProperty); }
set { SetValue(PlacementModeProperty, value); }
}
/// <summary>
/// Gets or sets the the anchor rectangle within the parent that the context menu will be placed
/// relative to when <see cref="PlacementMode"/> is <see cref="PlacementMode.AnchorAndGravity"/>.
/// </summary>
/// <remarks>
/// The placement rect defines a rectangle relative to <see cref="PlacementTarget"/> around
/// which the popup will be opened, with <see cref="PlacementAnchor"/> determining which edge
/// of the placement target is used.
///
/// If unset, the anchor rectangle will be the bounds of the <see cref="PlacementTarget"/>.
/// </remarks>
public Rect? PlacementRect
{
get { return GetValue(PlacementRectProperty); }
set { SetValue(PlacementRectProperty, value); }
}
/// <summary>
/// Gets or sets the control that is used to determine the popup's position.
/// </summary>
public Control? PlacementTarget
{
get { return GetValue(PlacementTargetProperty); }
set { SetValue(PlacementTargetProperty, value); }
}
/// <summary>
/// Occurs when the value of the
/// <see cref="P:Avalonia.Controls.ContextMenu.IsOpen" />
/// property is changing from false to true.
/// </summary>
public event CancelEventHandler ContextMenuOpening;
public event CancelEventHandler? ContextMenuOpening;
/// <summary>
/// Occurs when the value of the
/// <see cref="P:Avalonia.Controls.ContextMenu.IsOpen" />
/// property is changing from true to false.
/// </summary>
public event CancelEventHandler ContextMenuClosing;
public event CancelEventHandler? ContextMenuClosing;
/// <summary>
/// Called when the <see cref="Control.ContextMenu"/> property changes on a control.
@ -77,7 +210,7 @@ namespace Avalonia.Controls
{
control.PointerReleased -= ControlPointerReleased;
oldMenu._attachedControls?.Remove(control);
((ISetLogicalParent)oldMenu._popup)?.SetParent(null);
((ISetLogicalParent?)oldMenu._popup)?.SetParent(null);
}
if (e.NewValue is ContextMenu newMenu)
@ -97,7 +230,7 @@ namespace Avalonia.Controls
/// Opens a context menu on the specified control.
/// </summary>
/// <param name="control">The control.</param>
public void Open(Control control)
public void Open(Control? control)
{
if (control is null && (_attachedControls is null || _attachedControls.Count == 0))
{
@ -113,7 +246,7 @@ namespace Avalonia.Controls
nameof(control));
}
control ??= _attachedControls[0];
control ??= _attachedControls![0];
if (IsOpen)
{
@ -124,8 +257,14 @@ namespace Avalonia.Controls
{
_popup = new Popup
{
PlacementMode = PlacementMode.Pointer,
PlacementTarget = control,
HorizontalOffset = HorizontalOffset,
VerticalOffset = VerticalOffset,
PlacementAnchor = PlacementAnchor,
PlacementConstraintAdjustment = PlacementConstraintAdjustment,
PlacementGravity = PlacementGravity,
PlacementMode = PlacementMode,
PlacementRect = PlacementRect,
PlacementTarget = PlacementTarget ?? control,
StaysOpen = false
};
@ -204,7 +343,7 @@ namespace Avalonia.Controls
if (_attachedControls is null || _attachedControls.Count == 0)
{
((ISetLogicalParent)_popup).SetParent(null);
((ISetLogicalParent)_popup!).SetParent(null);
}
// HACK: Reset the focus when the popup is closed. We need to fix this so it's automatic.

22
src/Avalonia.Controls/Converters/MarginMultiplierConverter.cs

@ -18,10 +18,24 @@ namespace Avalonia.Controls.Converters
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (!(value is int depth))
return new Thickness(0);
return new Thickness(Left ? Indent * depth : 0, Top ? Indent * depth : 0, Right ? Indent * depth : 0, Bottom ? Indent * depth : 0);
if (value is int scalarDepth)
{
return new Thickness(
Left ? Indent * scalarDepth : 0,
Top ? Indent * scalarDepth : 0,
Right ? Indent * scalarDepth : 0,
Bottom ? Indent * scalarDepth : 0);
}
else if (value is Thickness thinknessDepth)
{
return new Thickness(
Left ? Indent * thinknessDepth.Left : 0,
Top ? Indent * thinknessDepth.Top : 0,
Right ? Indent * thinknessDepth.Right : 0,
Bottom ? Indent * thinknessDepth.Bottom : 0);
}
return new Thickness(0);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)

64
src/Avalonia.Controls/Converters/MenuScrollingVisibilityConverter.cs

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Controls.Primitives;
using Avalonia.Data.Converters;
using Avalonia.Utilities;
namespace Avalonia.Controls.Converters
{
public class MenuScrollingVisibilityConverter : IMultiValueConverter
{
public static readonly MenuScrollingVisibilityConverter Instance = new MenuScrollingVisibilityConverter();
public object Convert(IList<object> values, Type targetType, object parameter, CultureInfo culture)
{
if (parameter == null ||
values == null ||
values.Count != 4 ||
!(values[0] is ScrollBarVisibility visiblity) ||
!(values[1] is double offset) ||
!(values[2] is double extent) ||
!(values[3] is double viewport))
{
return AvaloniaProperty.UnsetValue;
}
if (visiblity == ScrollBarVisibility.Auto)
{
if (extent == viewport)
{
return false;
}
double target;
if (parameter is double d)
{
target = d;
}
else if (parameter is string s)
{
target = double.Parse(s, NumberFormatInfo.InvariantInfo);
}
else
{
return AvaloniaProperty.UnsetValue;
}
// Calculate the percent so that we can see if we are near the edge of the range
double percent = MathUtilities.Clamp(offset * 100.0 / (extent - viewport), 0, 100);
if (MathUtilities.AreClose(percent, target))
{
// We are at the end of the range, so no need for this button to be shown
return false;
}
return true;
}
return false;
}
}
}

6
src/Avalonia.Controls/LayoutTransformControl.cs

@ -14,8 +14,8 @@ namespace Avalonia.Controls
/// </summary>
public class LayoutTransformControl : Decorator
{
public static readonly StyledProperty<Transform> LayoutTransformProperty =
AvaloniaProperty.Register<LayoutTransformControl, Transform>(nameof(LayoutTransform));
public static readonly StyledProperty<ITransform> LayoutTransformProperty =
AvaloniaProperty.Register<LayoutTransformControl, ITransform>(nameof(LayoutTransform));
public static readonly StyledProperty<bool> UseRenderTransformProperty =
AvaloniaProperty.Register<LayoutTransformControl, bool>(nameof(LayoutTransform));
@ -37,7 +37,7 @@ namespace Avalonia.Controls
/// <summary>
/// Gets or sets a graphics transformation that should apply to this element when layout is performed.
/// </summary>
public Transform LayoutTransform
public ITransform LayoutTransform
{
get { return GetValue(LayoutTransformProperty); }
set { SetValue(LayoutTransformProperty, value); }

5
src/Avalonia.Controls/MenuItem.cs

@ -105,6 +105,7 @@ namespace Avalonia.Controls
static MenuItem()
{
SelectableMixin.Attach<MenuItem>(IsSelectedProperty);
PressedMixin.Attach<MenuItem>();
CommandProperty.Changed.Subscribe(CommandChanged);
FocusableProperty.OverrideDefaultValue<MenuItem>(true);
HeaderProperty.Changed.AddClassHandler<MenuItem>((x, e) => x.HeaderChanged(e));
@ -534,11 +535,13 @@ namespace Avalonia.Controls
if (oldValue != null)
{
LogicalChildren.Remove(oldValue);
PseudoClasses.Remove(":icon");
}
if (newValue != null)
{
LogicalChildren.Add(newValue);
PseudoClasses.Add(":icon");
}
}
@ -566,11 +569,13 @@ namespace Avalonia.Controls
{
RaiseEvent(new RoutedEventArgs(SubmenuOpenedEvent));
IsSelected = true;
PseudoClasses.Add(":open");
}
else
{
CloseSubmenus();
SelectedIndex = -1;
PseudoClasses.Remove(":open");
}
}

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

@ -1,5 +1,4 @@
using System;
using Avalonia.Controls;
namespace Avalonia.Platform
{
@ -46,9 +45,9 @@ namespace Avalonia.Platform
IPlatformHandle Handle { get; }
/// <summary>
/// Gets the maximum size of a window on the system.
/// Gets a maximum client size hint for an auto-sizing window, in device-independent pixels.
/// </summary>
Size MaxClientSize { get; }
Size MaxAutoSizeHint { get; }
/// <summary>
/// Sets whether this window appears on top of all other windows

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

@ -113,7 +113,7 @@ namespace Avalonia.Controls.Presenters
{
var scrollable = (ILogicalScrollable)Owner;
var visualRoot = Owner.GetVisualRoot();
var maxAvailableSize = (visualRoot as WindowBase)?.PlatformImpl?.MaxClientSize
var maxAvailableSize = (visualRoot as WindowBase)?.PlatformImpl?.MaxAutoSizeHint
?? (visualRoot as TopLevel)?.ClientSize;
// If infinity is passed as the available size and we're virtualized then we need to

55
src/Avalonia.Controls/Primitives/IPopupHost.cs

@ -5,19 +5,70 @@ using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives
{
/// <summary>
/// Represents the top-level control opened by a <see cref="Popup"/>.
/// </summary>
/// <remarks>
/// A popup host can be either be a popup window created by the operating system
/// (<see cref="PopupRoot"/>) or an <see cref="OverlayPopupHost"/> which is created
/// on an <see cref="OverlayLayer"/>.
/// </remarks>
public interface IPopupHost : IDisposable
{
/// <summary>
/// Sets the control to display in the popup.
/// </summary>
/// <param name="control"></param>
void SetChild(IControl control);
/// <summary>
/// Gets the presenter from the control's template.
/// </summary>
IContentPresenter Presenter { get; }
/// <summary>
/// Gets the root of the visual tree in the case where the popup is presented using a
/// separate visual tree.
/// </summary>
IVisual HostedVisualTreeRoot { get; }
/// <summary>
/// Raised when the control's template is applied.
/// </summary>
event EventHandler<TemplateAppliedEventArgs> TemplateApplied;
/// <summary>
/// Configures the position of the popup according to a target control and a set of
/// placement parameters.
/// </summary>
/// <param name="target">The placement target.</param>
/// <param name="placement">The placement mode.</param>
/// <param name="offset">The offset, in device-independent pixels.</param>
/// <param name="anchor">The anchor point.</param>
/// <param name="gravity">The popup gravity.</param>
/// <param name="rect">
/// The anchor rect. If null, the bounds of <paramref name="target"/> will be used.
/// </param>
void ConfigurePosition(IVisual target, PlacementMode placement, Point offset,
PopupPositioningEdge anchor = PopupPositioningEdge.None,
PopupPositioningEdge gravity = PopupPositioningEdge.None);
PopupAnchor anchor = PopupAnchor.None,
PopupGravity gravity = PopupGravity.None,
PopupPositionerConstraintAdjustment constraintAdjustment = PopupPositionerConstraintAdjustment.All,
Rect? rect = null);
/// <summary>
/// Shows the popup.
/// </summary>
void Show();
/// <summary>
/// Hides the popup.
/// </summary>
void Hide();
/// <summary>
/// Binds the constraints of the popup host to a set of properties, usally those present on
/// <see cref="Popup"/>.
/// </summary>
IDisposable BindConstraints(AvaloniaObject popup, StyledProperty<double> widthProperty,
StyledProperty<double> minWidthProperty, StyledProperty<double> maxWidthProperty,
StyledProperty<double> heightProperty, StyledProperty<double> minHeightProperty,

12
src/Avalonia.Controls/Primitives/OverlayPopupHost.cs

@ -71,10 +71,12 @@ namespace Avalonia.Controls.Primitives
}
public void ConfigurePosition(IVisual target, PlacementMode placement, Point offset,
PopupPositioningEdge anchor = PopupPositioningEdge.None, PopupPositioningEdge gravity = PopupPositioningEdge.None)
PopupAnchor anchor = PopupAnchor.None, PopupGravity gravity = PopupGravity.None,
PopupPositionerConstraintAdjustment constraintAdjustment = PopupPositionerConstraintAdjustment.All,
Rect? rect = null)
{
_positionerParameters.ConfigurePosition((TopLevel)_overlayLayer.GetVisualRoot(), target, placement, offset, anchor,
gravity);
gravity, constraintAdjustment, rect);
UpdatePosition();
}
@ -122,10 +124,8 @@ namespace Avalonia.Controls.Primitives
}, DispatcherPriority.Layout);
}
Point IManagedPopupPositionerPopup.TranslatePoint(Point pt) => pt;
Size IManagedPopupPositionerPopup.TranslateSize(Size size) => size;
double IManagedPopupPositionerPopup.Scaling => 1;
public static IPopupHost CreatePopupHost(IVisual target, IAvaloniaDependencyResolver dependencyResolver)
{
var platform = (target.GetVisualRoot() as TopLevel)?.PlatformImpl?.CreatePopup();

117
src/Avalonia.Controls/Primitives/Popup.cs

@ -3,6 +3,7 @@ using System.Diagnostics;
using System.Linq;
using System.Reactive.Disposables;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Interactivity;
@ -37,12 +38,45 @@ namespace Avalonia.Controls.Primitives
o => o.IsOpen,
(o, v) => o.IsOpen = v);
/// <summary>
/// Defines the <see cref="PlacementAnchor"/> property.
/// </summary>
public static readonly StyledProperty<PopupAnchor> PlacementAnchorProperty =
AvaloniaProperty.Register<Popup, PopupAnchor>(nameof(PlacementAnchor));
/// <summary>
/// Defines the <see cref="PlacementConstraintAdjustment"/> property.
/// </summary>
public static readonly StyledProperty<PopupPositionerConstraintAdjustment> PlacementConstraintAdjustmentProperty =
AvaloniaProperty.Register<Popup, PopupPositionerConstraintAdjustment>(
nameof(PlacementConstraintAdjustment),
PopupPositionerConstraintAdjustment.FlipX | PopupPositionerConstraintAdjustment.FlipY |
PopupPositionerConstraintAdjustment.ResizeX | PopupPositionerConstraintAdjustment.ResizeY);
/// <summary>
/// Defines the <see cref="PlacementGravity"/> property.
/// </summary>
public static readonly StyledProperty<PopupGravity> PlacementGravityProperty =
AvaloniaProperty.Register<Popup, PopupGravity>(nameof(PlacementGravity));
/// <summary>
/// Defines the <see cref="PlacementMode"/> property.
/// </summary>
public static readonly StyledProperty<PlacementMode> PlacementModeProperty =
AvaloniaProperty.Register<Popup, PlacementMode>(nameof(PlacementMode), defaultValue: PlacementMode.Bottom);
/// <summary>
/// Defines the <see cref="PlacementRect"/> property.
/// </summary>
public static readonly StyledProperty<Rect?> PlacementRectProperty =
AvaloniaProperty.Register<Popup, Rect?>(nameof(PlacementRect));
/// <summary>
/// Defines the <see cref="PlacementTarget"/> property.
/// </summary>
public static readonly StyledProperty<Control?> PlacementTargetProperty =
AvaloniaProperty.Register<Popup, Control?>(nameof(PlacementTarget));
#pragma warning disable 618
/// <summary>
/// Defines the <see cref="ObeyScreenEdges"/> property.
@ -63,12 +97,6 @@ namespace Avalonia.Controls.Primitives
public static readonly StyledProperty<double> VerticalOffsetProperty =
AvaloniaProperty.Register<Popup, double>(nameof(VerticalOffset));
/// <summary>
/// Defines the <see cref="PlacementTarget"/> property.
/// </summary>
public static readonly StyledProperty<Control?> PlacementTargetProperty =
AvaloniaProperty.Register<Popup, Control?>(nameof(PlacementTarget));
/// <summary>
/// Defines the <see cref="StaysOpen"/> property.
/// </summary>
@ -145,6 +173,36 @@ namespace Avalonia.Controls.Primitives
set { SetAndRaise(IsOpenProperty, ref _isOpen, value); }
}
/// <summary>
/// Gets or sets the anchor point on the <see cref="PlacementRect"/> when <see cref="PlacementMode"/>
/// is <see cref="PlacementMode.AnchorAndGravity"/>.
/// </summary>
public PopupAnchor PlacementAnchor
{
get { return GetValue(PlacementAnchorProperty); }
set { SetValue(PlacementAnchorProperty, value); }
}
/// <summary>
/// Gets or sets a value describing how the popup position will be adjusted if the
/// unadjusted position would result in the popup being partly constrained.
/// </summary>
public PopupPositionerConstraintAdjustment PlacementConstraintAdjustment
{
get { return GetValue(PlacementConstraintAdjustmentProperty); }
set { SetValue(PlacementConstraintAdjustmentProperty, value); }
}
/// <summary>
/// Gets or sets a value which defines in what direction the popup should open
/// when <see cref="PlacementMode"/> is <see cref="PlacementMode.AnchorAndGravity"/>.
/// </summary>
public PopupGravity PlacementGravity
{
get { return GetValue(PlacementGravityProperty); }
set { SetValue(PlacementGravityProperty, value); }
}
/// <summary>
/// Gets or sets the placement mode of the popup in relation to the <see cref="PlacementTarget"/>.
/// </summary>
@ -154,6 +212,32 @@ namespace Avalonia.Controls.Primitives
set { SetValue(PlacementModeProperty, value); }
}
/// <summary>
/// Gets or sets the the anchor rectangle within the parent that the popup will be placed
/// relative to when <see cref="PlacementMode"/> is <see cref="PlacementMode.AnchorAndGravity"/>.
/// </summary>
/// <remarks>
/// The placement rect defines a rectangle relative to <see cref="PlacementTarget"/> around
/// which the popup will be opened, with <see cref="PlacementAnchor"/> determining which edge
/// of the placement target is used.
///
/// If unset, the anchor rectangle will be the bounds of the <see cref="PlacementTarget"/>.
/// </remarks>
public Rect? PlacementRect
{
get { return GetValue(PlacementRectProperty); }
set { SetValue(PlacementRectProperty, value); }
}
/// <summary>
/// Gets or sets the control that is used to determine the popup's position.
/// </summary>
public Control? PlacementTarget
{
get { return GetValue(PlacementTargetProperty); }
set { SetValue(PlacementTargetProperty, value); }
}
[Obsolete("This property has no effect")]
public bool ObeyScreenEdges
{
@ -162,7 +246,7 @@ namespace Avalonia.Controls.Primitives
}
/// <summary>
/// Gets or sets the Horizontal offset of the popup in relation to the <see cref="PlacementTarget"/>
/// Gets or sets the Horizontal offset of the popup in relation to the <see cref="PlacementTarget"/>.
/// </summary>
public double HorizontalOffset
{
@ -171,7 +255,7 @@ namespace Avalonia.Controls.Primitives
}
/// <summary>
/// Gets or sets the Vertical offset of the popup in relation to the <see cref="PlacementTarget"/>
/// Gets or sets the Vertical offset of the popup in relation to the <see cref="PlacementTarget"/>.
/// </summary>
public double VerticalOffset
{
@ -179,15 +263,6 @@ namespace Avalonia.Controls.Primitives
set { SetValue(VerticalOffsetProperty, value); }
}
/// <summary>
/// Gets or sets the control that is used to determine the popup's position.
/// </summary>
public Control? PlacementTarget
{
get { return GetValue(PlacementTargetProperty); }
set { SetValue(PlacementTargetProperty, value); }
}
/// <summary>
/// Gets or sets a value indicating whether the popup should stay open when the popup is
/// pressed or loses focus.
@ -260,8 +335,12 @@ namespace Avalonia.Controls.Primitives
popupHost.ConfigurePosition(
placementTarget,
PlacementMode,
new Point(HorizontalOffset, VerticalOffset));
PlacementMode,
new Point(HorizontalOffset, VerticalOffset),
PlacementAnchor,
PlacementGravity,
PlacementConstraintAdjustment,
PlacementRect);
DeferCleanup(SubscribeToEventHandler<IPopupHost, EventHandler<TemplateAppliedEventArgs>>(popupHost, RootTemplateApplied,
(x, handler) => x.TemplateApplied += handler,

424
src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs

@ -50,46 +50,48 @@ using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives.PopupPositioning
{
/// <summary>
///
/// The IPopupPositioner provides a collection of rules for the placement of a
/// a popup relative to its parent. Rules can be defined to ensure
/// the popup remains within the visible area's borders, and to
/// specify how the popup changes its position, such as sliding along
/// an axis, or flipping around a rectangle. These positioner-created rules are
/// constrained by the requirement that a popup must intersect with or
/// be at least partially adjacent to its parent surface.
/// Provides positioning parameters to <see cref="IPopupPositioner"/>.
/// </summary>
/// <remarks>
/// The IPopupPositioner provides a collection of rules for the placement of a a popup relative
/// to its parent. Rules can be defined to ensure the popup remains within the visible area's
/// borders, and to specify how the popup changes its position, such as sliding along an axis,
/// or flipping around a rectangle. These positioner-created rules are constrained by the
/// requirement that a popup must intersect with or be at least partially adjacent to its parent
/// surface.
/// </remarks>
public struct PopupPositionerParameters
{
private PopupPositioningEdge _gravity;
private PopupPositioningEdge _anchor;
private PopupGravity _gravity;
private PopupAnchor _anchor;
/// <summary>
/// Set the size of the popup that is to be positioned with the positioner
/// object. The size is in scaled coordinates.
/// Set the size of the popup that is to be positioned with the positioner object, in device-
/// independent pixels.
/// </summary>
public Size Size { get; set; }
/// <summary>
/// Specify the anchor rectangle within the parent that the popup
/// will be placed relative to. The rectangle is relative to the
/// parent geometry
///
/// The anchor rectangle may not extend outside the window geometry of the
/// popup's parent. The anchor rectangle is in scaled coordinates
/// Specifies the anchor rectangle within the parent that the popup will be placed relative
/// to, in device-independent pixels.
/// </summary>
/// <remarks>
/// The rectangle is relative to the parent geometry and may not extend outside the window
/// geometry of the popup's parent.
/// </remarks>
public Rect AnchorRectangle { get; set; }
/// <summary>
/// Defines the anchor point for the anchor rectangle. The specified anchor
/// is used derive an anchor point that the popup will be
/// positioned relative to. If a corner anchor is set (e.g. 'TopLeft' or
/// 'BottomRight'), the anchor point will be at the specified corner;
/// otherwise, the derived anchor point will be centered on the specified
/// edge, or in the center of the anchor rectangle if no edge is specified.
/// Defines the anchor point for the anchor rectangle.
/// </summary>
public PopupPositioningEdge Anchor
/// <remarks>
/// The specified anchor is used derive an anchor point that the popup will be positioned
/// relative to. If a corner anchor is set (e.g. 'TopLeft' or 'BottomRight'), the anchor
/// point will be at the specified corner; otherwise, the derived anchor point will be
/// centered on the specified edge, or in the center of the anchor rectangle if no edge is
/// specified.
/// </remarks>
public PopupAnchor Anchor
{
get => _anchor;
set
@ -100,66 +102,70 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
}
/// <summary>
/// Defines in what direction a popup should be positioned, relative to
/// the anchor point of the parent. If a corner gravity is
/// specified (e.g. 'BottomRight' or 'TopLeft'), then the popup
/// will be placed towards the specified gravity; otherwise, the popup
/// will be centered over the anchor point on any axis that had no
/// gravity specified.
/// Defines in what direction a popup should be positioned, relative to the anchor point of
/// the parent.
/// </summary>
public PopupPositioningEdge Gravity
/// <remarks>
/// If a corner gravity is specified (e.g. 'BottomRight' or 'TopLeft'), then the popup will
/// be placed towards the specified gravity; otherwise, the popup will be centered over the
/// anchor point on any axis that had no gravity specified.
/// </remarks>
public PopupGravity Gravity
{
get => _gravity;
set
{
PopupPositioningEdgeHelper.ValidateEdge(value);
PopupPositioningEdgeHelper.ValidateGravity(value);
_gravity = value;
}
}
/// <summary>
/// Specify how the popup should be positioned if the originally intended
/// position caused the popup to be constrained, meaning at least
/// partially outside positioning boundaries set by the positioner. The
/// adjustment is set by constructing a bitmask describing the adjustment to
/// be made when the popup is constrained on that axis.
/// Specify how the popup should be positioned if the originally intended position caused
/// the popup to be constrained.
/// </summary>
/// <remarks>
/// Adjusts the popup position if the intended position caused the popup to be constrained;
/// meaning at least partially outside positioning boundaries set by the positioner. The
/// adjustment is set by constructing a bitmask describing the adjustment to be made when
/// the popup is constrained on that axis.
///
/// If no bit for one axis is set, the positioner will assume that the child
/// surface should not change its position on that axis when constrained.
/// If no bit for one axis is set, the positioner will assume that the child surface should
/// not change its position on that axis when constrained.
///
/// If more than one bit for one axis is set, the order of how adjustments
/// are applied is specified in the corresponding adjustment descriptions.
/// If more than one bit for one axis is set, the order of how adjustments are applied is
/// specified in the corresponding adjustment descriptions.
///
/// The default adjustment is none.
/// </summary>
/// </remarks>
public PopupPositionerConstraintAdjustment ConstraintAdjustment { get; set; }
/// <summary>
/// Specify the popup position offset relative to the position of the
/// anchor on the anchor rectangle and the anchor on the popup. For
/// example if the anchor of the anchor rectangle is at (x, y), the popup
/// has the gravity bottom|right, and the offset is (ox, oy), the calculated
/// surface position will be (x + ox, y + oy). The offset position of the
/// surface is the one used for constraint testing. See
/// set_constraint_adjustment.
///
/// An example use case is placing a popup menu on top of a user interface
/// element, while aligning the user interface element of the parent surface
/// with some user interface element placed somewhere in the popup.
/// anchor on the anchor rectangle and the anchor on the popup.
/// </summary>
/// <remarks>
/// For example if the anchor of the anchor rectangle is at (x, y), the popup has the
/// gravity bottom|right, and the offset is (ox, oy), the calculated surface position will
/// be (x + ox, y + oy). The offset position of the surface is the one used for constraint
/// testing. See set_constraint_adjustment.
///
/// An example use case is placing a popup menu on top of a user interface element, while
/// aligning the user interface element of the parent surface with some user interface
/// element placed somewhere in the popup.
/// </remarks>
public Point Offset { get; set; }
}
/// <summary>
/// The constraint adjustment value define ways how popup position will
/// be adjusted if the unadjusted position would result in the popup
/// being partly constrained.
///
/// Whether a popup is considered 'constrained' is left to the positioner
/// to determine. For example, the popup may be partly outside the
/// target platform defined 'work area', thus necessitating the popup's
/// position be adjusted until it is entirely inside the work area.
/// Defines how a popup position will be adjusted if the unadjusted position would result in
/// the popup being partly constrained.
/// </summary>
/// <remarks>
/// Whether a popup is considered 'constrained' is left to the positioner to determine. For
/// example, the popup may be partly outside the target platform defined 'work area', thus
/// necessitating the popup's position be adjusted until it is entirely inside the work area.
/// </remarks>
[Flags]
public enum PopupPositionerConstraintAdjustment
{
@ -171,79 +177,97 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
/// <summary>
/// Slide the surface along the x axis until it is no longer constrained.
/// First try to slide towards the direction of the gravity on the x axis
/// until either the edge in the opposite direction of the gravity is
/// unconstrained or the edge in the direction of the gravity is
/// constrained.
///
/// Then try to slide towards the opposite direction of the gravity on the
/// x axis until either the edge in the direction of the gravity is
/// unconstrained or the edge in the opposite direction of the gravity is
/// constrained.
/// </summary>
/// <remarks>
/// First try to slide towards the direction of the gravity on the x axis until either the
/// edge in the opposite direction of the gravity is unconstrained or the edge in the
/// direction of the gravity is constrained.
///
/// Then try to slide towards the opposite direction of the gravity on the x axis until
/// either the edge in the direction of the gravity is unconstrained or the edge in the
/// opposite direction of the gravity is constrained.
/// </remarks>
SlideX = 1,
/// <summary>
/// Slide the surface along the y axis until it is no longer constrained.
///
/// First try to slide towards the direction of the gravity on the y axis
/// until either the edge in the opposite direction of the gravity is
/// unconstrained or the edge in the direction of the gravity is
/// constrained.
///
/// Then try to slide towards the opposite direction of the gravity on the
/// y axis until either the edge in the direction of the gravity is
/// unconstrained or the edge in the opposite direction of the gravity is
/// constrained.
/// */
/// Slide the surface along the y axis until it is no longer constrained.
/// </summary>
/// <remarks>
/// First try to slide towards the direction of the gravity on the y axis until either the
/// edge in the opposite direction of the gravity is unconstrained or the edge in the
/// direction of the gravity is constrained.
///
/// Then try to slide towards the opposite direction of the gravity on the y axis until
/// either the edge in the direction of the gravity is unconstrained or the edge in the
/// opposite direction of the gravity is constrained.
/// </remarks>
SlideY = 2,
/// <summary>
/// Invert the anchor and gravity on the x axis if the surface is
/// constrained on the x axis. For example, if the left edge of the
/// surface is constrained, the gravity is 'left' and the anchor is
/// 'left', change the gravity to 'right' and the anchor to 'right'.
///
/// If the adjusted position also ends up being constrained, the resulting
/// position of the flip_x adjustment will be the one before the
/// adjustment.
/// Invert the anchor and gravity on the x axis if the surface is constrained on the x axis.
/// </summary>
/// <remarks>
/// For example, if the left edge of the surface is constrained, the gravity is 'left' and
/// the anchor is 'left', change the gravity to 'right' and the anchor to 'right'.
///
/// If the adjusted position also ends up being constrained, the resulting position of the
/// FlipX adjustment will be the one before the adjustment.
/// /// </remarks>
FlipX = 4,
/// <summary>
/// Invert the anchor and gravity on the y axis if the surface is
/// constrained on the y axis. For example, if the bottom edge of the
/// surface is constrained, the gravity is 'bottom' and the anchor is
/// 'bottom', change the gravity to 'top' and the anchor to 'top'.
/// Invert the anchor and gravity on the y axis if the surface is constrained on the y axis.
/// </summary>
/// <remarks>
/// For example, if the bottom edge of the surface is constrained, the gravity is 'bottom'
/// and the anchor is 'bottom', change the gravity to 'top' and the anchor to 'top'.
///
/// The adjusted position is calculated given the original anchor
/// rectangle and offset, but with the new flipped anchor and gravity
/// values.
/// The adjusted position is calculated given the original anchor rectangle and offset, but
/// with the new flipped anchor and gravity values.
///
/// If the adjusted position also ends up being constrained, the resulting
/// position of the flip_y adjustment will be the one before the
/// adjustment.
/// </summary>
/// If the adjusted position also ends up being constrained, the resulting position of the
/// FlipY adjustment will be the one before the adjustment.
/// </remarks>
FlipY = 8,
All = SlideX|SlideY|FlipX|FlipY
/// <summary>
/// Horizontally resize the surface
/// </summary>
/// <remarks>
/// Resize the surface horizontally so that it is completely unconstrained.
/// </remarks>
ResizeX = 16,
/// <summary>
/// Vertically resize the surface
/// </summary>
/// <remarks>
/// Resize the surface vertically so that it is completely unconstrained.
/// </remarks>
ResizeY = 16,
All = SlideX|SlideY|FlipX|FlipY|ResizeX|ResizeY
}
static class PopupPositioningEdgeHelper
{
public static void ValidateEdge(this PopupPositioningEdge edge)
public static void ValidateEdge(this PopupAnchor edge)
{
if (((edge & PopupPositioningEdge.Left) != 0 && (edge & PopupPositioningEdge.Right) != 0)
if (((edge & PopupAnchor.Left) != 0 && (edge & PopupAnchor.Right) != 0)
||
((edge & PopupPositioningEdge.Top) != 0 && (edge & PopupPositioningEdge.Bottom) != 0))
((edge & PopupAnchor.Top) != 0 && (edge & PopupAnchor.Bottom) != 0))
throw new ArgumentException("Opposite edges specified");
}
public static PopupPositioningEdge Flip(this PopupPositioningEdge edge)
public static void ValidateGravity(this PopupGravity gravity)
{
ValidateEdge((PopupAnchor)gravity);
}
public static PopupAnchor Flip(this PopupAnchor edge)
{
var hmask = PopupPositioningEdge.Left | PopupPositioningEdge.Right;
var vmask = PopupPositioningEdge.Top | PopupPositioningEdge.Bottom;
var hmask = PopupAnchor.Left | PopupAnchor.Right;
var vmask = PopupAnchor.Top | PopupAnchor.Bottom;
if ((edge & hmask) != 0)
edge ^= hmask;
if ((edge & vmask) != 0)
@ -251,43 +275,167 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
return edge;
}
public static PopupPositioningEdge FlipX(this PopupPositioningEdge edge)
public static PopupAnchor FlipX(this PopupAnchor edge)
{
if ((edge & PopupPositioningEdge.HorizontalMask) != 0)
edge ^= PopupPositioningEdge.HorizontalMask;
if ((edge & PopupAnchor.HorizontalMask) != 0)
edge ^= PopupAnchor.HorizontalMask;
return edge;
}
public static PopupPositioningEdge FlipY(this PopupPositioningEdge edge)
public static PopupAnchor FlipY(this PopupAnchor edge)
{
if ((edge & PopupPositioningEdge.VerticalMask) != 0)
edge ^= PopupPositioningEdge.VerticalMask;
if ((edge & PopupAnchor.VerticalMask) != 0)
edge ^= PopupAnchor.VerticalMask;
return edge;
}
public static PopupGravity FlipX(this PopupGravity gravity)
{
return (PopupGravity)FlipX((PopupAnchor)gravity);
}
public static PopupGravity FlipY(this PopupGravity gravity)
{
return (PopupGravity)FlipY((PopupAnchor)gravity);
}
}
/// <summary>
/// Defines the edges around an anchor rectangle on which a popup will open.
/// </summary>
[Flags]
public enum PopupPositioningEdge
public enum PopupAnchor
{
/// <summary>
/// The center of the anchor rectangle.
/// </summary>
None,
/// <summary>
/// The top edge of the anchor rectangle.
/// </summary>
Top = 1,
/// <summary>
/// The bottom edge of the anchor rectangle.
/// </summary>
Bottom = 2,
/// <summary>
/// The left edge of the anchor rectangle.
/// </summary>
Left = 4,
/// <summary>
/// The right edge of the anchor rectangle.
/// </summary>
Right = 8,
/// <summary>
/// The top-left corner of the anchor rectangle.
/// </summary>
TopLeft = Top | Left,
/// <summary>
/// The top-right corner of the anchor rectangle.
/// </summary>
TopRight = Top | Right,
/// <summary>
/// The bottom-left corner of the anchor rectangle.
/// </summary>
BottomLeft = Bottom | Left,
/// <summary>
/// The bottom-right corner of the anchor rectangle.
/// </summary>
BottomRight = Bottom | Right,
/// <summary>
/// A mask for the vertical component flags.
/// </summary>
VerticalMask = Top | Bottom,
/// <summary>
/// A mask for the horizontal component flags.
/// </summary>
HorizontalMask = Left | Right,
/// <summary>
/// A mask for all flags.
/// </summary>
AllMask = VerticalMask|HorizontalMask
}
/// <summary>
/// Defines the direction in which a popup will open.
/// </summary>
[Flags]
public enum PopupGravity
{
/// <summary>
/// The popup will be centered over the anchor edge.
/// </summary>
None,
/// <summary>
/// The popup will be positioned above the anchor edge
/// </summary>
Top = 1,
/// <summary>
/// The popup will be positioned below the anchor edge
/// </summary>
Bottom = 2,
/// <summary>
/// The popup will be positioned to the left of the anchor edge
/// </summary>
Left = 4,
/// <summary>
/// The popup will be positioned to the right of the anchor edge
/// </summary>
Right = 8,
/// <summary>
/// The popup will be positioned to the top-left of the anchor edge
/// </summary>
TopLeft = Top | Left,
/// <summary>
/// The popup will be positioned to the top-right of the anchor edge
/// </summary>
TopRight = Top | Right,
/// <summary>
/// The popup will be positioned to the bottom-left of the anchor edge
/// </summary>
BottomLeft = Bottom | Left,
/// <summary>
/// The popup will be positioned to the bottom-right of the anchor edge
/// </summary>
BottomRight = Bottom | Right,
}
/// <summary>
/// Positions an <see cref="IPopupHost"/>.
/// </summary>
/// <remarks>
/// <see cref="IPopupPositioner"/> is an abstraction of the wayland xdg_positioner spec.
///
/// The popup positioner implementation is determined by the platform implementation. A default
/// managed implementation is provided in <see cref="ManagedPopupPositioner"/> for platforms
/// on which popups can be arbitrarily positioned.
/// </remarks>
public interface IPopupPositioner
{
/// <summary>
/// Updates the position of the associated <see cref="IPopupHost"/> according to the
/// specified parameters.
/// </summary>
/// <param name="parameters">The positioning parameters.</param>
void Update(PopupPositionerParameters parameters);
}
@ -296,18 +444,19 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
public static void ConfigurePosition(ref this PopupPositionerParameters positionerParameters,
TopLevel topLevel,
IVisual target, PlacementMode placement, Point offset,
PopupPositioningEdge anchor, PopupPositioningEdge gravity)
PopupAnchor anchor, PopupGravity gravity,
PopupPositionerConstraintAdjustment constraintAdjustment, Rect? rect)
{
// We need a better way for tracking the last pointer position
var pointer = topLevel.PointToClient(topLevel.PlatformImpl.MouseDevice.Position);
positionerParameters.Offset = offset;
positionerParameters.ConstraintAdjustment = PopupPositionerConstraintAdjustment.All;
positionerParameters.ConstraintAdjustment = constraintAdjustment;
if (placement == PlacementMode.Pointer)
{
positionerParameters.AnchorRectangle = new Rect(pointer, new Size(1, 1));
positionerParameters.Anchor = PopupPositioningEdge.TopLeft;
positionerParameters.Gravity = PopupPositioningEdge.BottomRight;
positionerParameters.Anchor = PopupAnchor.TopLeft;
positionerParameters.Gravity = PopupGravity.BottomRight;
}
else
{
@ -317,32 +466,33 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
if (matrix == null)
{
if (target.GetVisualRoot() == null)
throw new InvalidCastException("Target control is not attached to the visual tree");
throw new InvalidCastException("Target control is not in the same tree as the popup parent");
throw new InvalidOperationException("Target control is not attached to the visual tree");
throw new InvalidOperationException("Target control is not in the same tree as the popup parent");
}
positionerParameters.AnchorRectangle = new Rect(default, target.Bounds.Size)
.TransformToAABB(matrix.Value);
var bounds = new Rect(default, target.Bounds.Size);
var anchorRect = rect ?? bounds;
positionerParameters.AnchorRectangle = anchorRect.Intersect(bounds).TransformToAABB(matrix.Value);
if (placement == PlacementMode.Right)
{
positionerParameters.Anchor = PopupPositioningEdge.TopRight;
positionerParameters.Gravity = PopupPositioningEdge.BottomRight;
positionerParameters.Anchor = PopupAnchor.TopRight;
positionerParameters.Gravity = PopupGravity.BottomRight;
}
else if (placement == PlacementMode.Bottom)
{
positionerParameters.Anchor = PopupPositioningEdge.BottomLeft;
positionerParameters.Gravity = PopupPositioningEdge.BottomRight;
positionerParameters.Anchor = PopupAnchor.BottomLeft;
positionerParameters.Gravity = PopupGravity.BottomRight;
}
else if (placement == PlacementMode.Left)
{
positionerParameters.Anchor = PopupPositioningEdge.TopLeft;
positionerParameters.Gravity = PopupPositioningEdge.BottomLeft;
positionerParameters.Anchor = PopupAnchor.TopLeft;
positionerParameters.Gravity = PopupGravity.BottomLeft;
}
else if (placement == PlacementMode.Top)
{
positionerParameters.Anchor = PopupPositioningEdge.TopLeft;
positionerParameters.Gravity = PopupPositioningEdge.TopRight;
positionerParameters.Anchor = PopupAnchor.TopLeft;
positionerParameters.Gravity = PopupGravity.TopRight;
}
else if (placement == PlacementMode.AnchorAndGravity)
{

117
src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Transactions;
namespace Avalonia.Controls.Primitives.PopupPositioning
{
@ -8,9 +9,8 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
{
IReadOnlyList<ManagedPopupPositionerScreenInfo> Screens { get; }
Rect ParentClientAreaScreenGeometry { get; }
double Scaling { get; }
void MoveAndResize(Point devicePoint, Size virtualSize);
Point TranslatePoint(Point pt);
Size TranslateSize(Size size);
}
public class ManagedPopupPositionerScreenInfo
@ -25,6 +25,10 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
}
}
/// <summary>
/// An <see cref="IPopupPositioner"/> implementation for platforms on which a popup can be
/// aritrarily positioned.
/// </summary>
public class ManagedPopupPositioner : IPopupPositioner
{
private readonly IManagedPopupPositionerPopup _popup;
@ -35,38 +39,38 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
}
private static Point GetAnchorPoint(Rect anchorRect, PopupPositioningEdge edge)
private static Point GetAnchorPoint(Rect anchorRect, PopupAnchor edge)
{
double x, y;
if ((edge & PopupPositioningEdge.Left) != 0)
if ((edge & PopupAnchor.Left) != 0)
x = anchorRect.X;
else if ((edge & PopupPositioningEdge.Right) != 0)
else if ((edge & PopupAnchor.Right) != 0)
x = anchorRect.Right;
else
x = anchorRect.X + anchorRect.Width / 2;
if ((edge & PopupPositioningEdge.Top) != 0)
if ((edge & PopupAnchor.Top) != 0)
y = anchorRect.Y;
else if ((edge & PopupPositioningEdge.Bottom) != 0)
else if ((edge & PopupAnchor.Bottom) != 0)
y = anchorRect.Bottom;
else
y = anchorRect.Y + anchorRect.Height / 2;
return new Point(x, y);
}
private static Point Gravitate(Point anchorPoint, Size size, PopupPositioningEdge gravity)
private static Point Gravitate(Point anchorPoint, Size size, PopupGravity gravity)
{
double x, y;
if ((gravity & PopupPositioningEdge.Left) != 0)
if ((gravity & PopupGravity.Left) != 0)
x = -size.Width;
else if ((gravity & PopupPositioningEdge.Right) != 0)
else if ((gravity & PopupGravity.Right) != 0)
x = 0;
else
x = -size.Width / 2;
if ((gravity & PopupPositioningEdge.Top) != 0)
if ((gravity & PopupGravity.Top) != 0)
y = -size.Height;
else if ((gravity & PopupPositioningEdge.Bottom) != 0)
else if ((gravity & PopupGravity.Bottom) != 0)
y = 0;
else
y = -size.Height / 2;
@ -75,17 +79,24 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
public void Update(PopupPositionerParameters parameters)
{
Update(_popup.TranslateSize(parameters.Size), parameters.Size,
new Rect(_popup.TranslatePoint(parameters.AnchorRectangle.TopLeft),
_popup.TranslateSize(parameters.AnchorRectangle.Size)),
parameters.Anchor, parameters.Gravity, parameters.ConstraintAdjustment,
_popup.TranslatePoint(parameters.Offset));
var rect = Calculate(
parameters.Size * _popup.Scaling,
new Rect(
parameters.AnchorRectangle.TopLeft * _popup.Scaling,
parameters.AnchorRectangle.Size * _popup.Scaling),
parameters.Anchor,
parameters.Gravity,
parameters.ConstraintAdjustment,
parameters.Offset * _popup.Scaling);
_popup.MoveAndResize(
rect.Position,
rect.Size / _popup.Scaling);
}
private void Update(Size translatedSize, Size originalSize,
Rect anchorRect, PopupPositioningEdge anchor, PopupPositioningEdge gravity,
private Rect Calculate(Size translatedSize,
Rect anchorRect, PopupAnchor anchor, PopupGravity gravity,
PopupPositionerConstraintAdjustment constraintAdjustment, Point offset)
{
var parentGeometry = _popup.ParentClientAreaScreenGeometry;
@ -112,28 +123,30 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
var bounds = GetBounds();
bool FitsInBounds(Rect rc, PopupPositioningEdge edge = PopupPositioningEdge.AllMask)
bool FitsInBounds(Rect rc, PopupAnchor edge = PopupAnchor.AllMask)
{
if ((edge & PopupPositioningEdge.Left) != 0
if ((edge & PopupAnchor.Left) != 0
&& rc.X < bounds.X)
return false;
if ((edge & PopupPositioningEdge.Top) != 0
if ((edge & PopupAnchor.Top) != 0
&& rc.Y < bounds.Y)
return false;
if ((edge & PopupPositioningEdge.Right) != 0
if ((edge & PopupAnchor.Right) != 0
&& rc.Right > bounds.Right)
return false;
if ((edge & PopupPositioningEdge.Bottom) != 0
if ((edge & PopupAnchor.Bottom) != 0
&& rc.Bottom > bounds.Bottom)
return false;
return true;
}
Rect GetUnconstrained(PopupPositioningEdge a, PopupPositioningEdge g) =>
static bool IsValid(in Rect rc) => rc.Width > 0 && rc.Height > 0;
Rect GetUnconstrained(PopupAnchor a, PopupGravity g) =>
new Rect(Gravitate(GetAnchorPoint(anchorRect, a), translatedSize, g) + offset, translatedSize);
@ -141,11 +154,11 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
// If flipping geometry and anchor is allowed and helps, use the flipped one,
// otherwise leave it as is
if (!FitsInBounds(geo, PopupPositioningEdge.HorizontalMask)
if (!FitsInBounds(geo, PopupAnchor.HorizontalMask)
&& (constraintAdjustment & PopupPositionerConstraintAdjustment.FlipX) != 0)
{
var flipped = GetUnconstrained(anchor.FlipX(), gravity.FlipX());
if (FitsInBounds(flipped, PopupPositioningEdge.HorizontalMask))
if (FitsInBounds(flipped, PopupAnchor.HorizontalMask))
geo = geo.WithX(flipped.X);
}
@ -157,13 +170,34 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
geo = geo.WithX(bounds.Right - geo.Width);
}
// Resize the rect horizontally if allowed.
if ((constraintAdjustment & PopupPositionerConstraintAdjustment.ResizeX) != 0)
{
var unconstrainedRect = geo;
if (!FitsInBounds(unconstrainedRect, PopupAnchor.Left))
{
unconstrainedRect = unconstrainedRect.WithX(bounds.X);
}
if (!FitsInBounds(unconstrainedRect, PopupAnchor.Right))
{
unconstrainedRect = unconstrainedRect.WithWidth(bounds.Width - unconstrainedRect.X);
}
if (IsValid(unconstrainedRect))
{
geo = unconstrainedRect;
}
}
// If flipping geometry and anchor is allowed and helps, use the flipped one,
// otherwise leave it as is
if (!FitsInBounds(geo, PopupPositioningEdge.VerticalMask)
if (!FitsInBounds(geo, PopupAnchor.VerticalMask)
&& (constraintAdjustment & PopupPositionerConstraintAdjustment.FlipY) != 0)
{
var flipped = GetUnconstrained(anchor.FlipY(), gravity.FlipY());
if (FitsInBounds(flipped, PopupPositioningEdge.VerticalMask))
if (FitsInBounds(flipped, PopupAnchor.VerticalMask))
geo = geo.WithY(flipped.Y);
}
@ -175,7 +209,28 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
geo = geo.WithY(bounds.Bottom - geo.Height);
}
_popup.MoveAndResize(geo.TopLeft, originalSize);
// Resize the rect vertically if allowed.
if ((constraintAdjustment & PopupPositionerConstraintAdjustment.ResizeY) != 0)
{
var unconstrainedRect = geo;
if (!FitsInBounds(unconstrainedRect, PopupAnchor.Top))
{
unconstrainedRect = unconstrainedRect.WithY(bounds.Y);
}
if (!FitsInBounds(unconstrainedRect, PopupAnchor.Bottom))
{
unconstrainedRect = unconstrainedRect.WithHeight(bounds.Height - unconstrainedRect.Y);
}
if (IsValid(unconstrainedRect))
{
geo = unconstrainedRect;
}
}
return geo;
}
}
}

6
src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs

@ -32,7 +32,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
{
// Popup positioner operates with abstract coordinates, but in our case they are pixel ones
var point = _parent.PointToScreen(default);
var size = TranslateSize(_parent.ClientSize);
var size = _parent.ClientSize * Scaling;
return new Rect(point.X, point.Y, size.Width, size.Height);
}
@ -43,8 +43,6 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
_moveResize(new PixelPoint((int)devicePoint.X, (int)devicePoint.Y), virtualSize, _parent.Scaling);
}
public virtual Point TranslatePoint(Point pt) => pt * _parent.Scaling;
public virtual Size TranslateSize(Size size) => size * _parent.Scaling;
public virtual double Scaling => _parent.Scaling;
}
}

23
src/Avalonia.Controls/Primitives/PopupRoot.cs

@ -82,11 +82,13 @@ namespace Avalonia.Controls.Primitives
}
public void ConfigurePosition(IVisual target, PlacementMode placement, Point offset,
PopupPositioningEdge anchor = PopupPositioningEdge.None,
PopupPositioningEdge gravity = PopupPositioningEdge.None)
PopupAnchor anchor = PopupAnchor.None,
PopupGravity gravity = PopupGravity.None,
PopupPositionerConstraintAdjustment constraintAdjustment = PopupPositionerConstraintAdjustment.All,
Rect? rect = null)
{
_positionerParameters.ConfigurePosition(_parent, target,
placement, offset, anchor, gravity);
placement, offset, anchor, gravity, constraintAdjustment, rect);
if (_positionerParameters.Size != default)
UpdatePosition();
@ -119,7 +121,20 @@ namespace Avalonia.Controls.Primitives
protected override Size MeasureOverride(Size availableSize)
{
var measured = base.MeasureOverride(availableSize);
var maxAutoSize = PlatformImpl?.MaxAutoSizeHint ?? Size.Infinity;
var constraint = availableSize;
if (double.IsInfinity(constraint.Width))
{
constraint = constraint.WithWidth(maxAutoSize.Width);
}
if (double.IsInfinity(constraint.Height))
{
constraint = constraint.WithHeight(maxAutoSize.Height);
}
var measured = base.MeasureOverride(constraint);
var width = measured.Width;
var height = measured.Height;
var widthCache = Width;

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

@ -259,7 +259,7 @@ namespace Avalonia.Controls.Primitives
CoerceLength(ref increaseButtonLength, arrangeSize.Width);
CoerceLength(ref thumbLength, arrangeSize.Width);
offset = offset.WithY(isDirectionReversed ? increaseButtonLength + thumbLength : 0.0);
offset = offset.WithX(isDirectionReversed ? increaseButtonLength + thumbLength : 0.0);
pieceSize = pieceSize.WithWidth(decreaseButtonLength);
if (DecreaseButton != null)

32
src/Avalonia.Controls/ScrollViewer.cs

@ -387,6 +387,38 @@ namespace Avalonia.Controls
/// <inheritdoc/>
IControl IScrollAnchorProvider.CurrentAnchor => null; // TODO: Implement
/// <summary>
/// Scrolls the content up one line.
/// </summary>
public void LineUp()
{
Offset -= new Vector(0, _smallChange.Height);
}
/// <summary>
/// Scrolls the content down one line.
/// </summary>
public void LineDown()
{
Offset += new Vector(0, _smallChange.Height);
}
/// <summary>
/// Scrolls the content left one line.
/// </summary>
public void LineLeft()
{
Offset -= new Vector(_smallChange.Width, 0);
}
/// <summary>
/// Scrolls the content right one line.
/// </summary>
public void LineRight()
{
Offset += new Vector(_smallChange.Width, 0);
}
/// <summary>
/// Scrolls to the top-left corner of the content.
/// </summary>

2
src/Avalonia.Controls/Slider.cs

@ -201,7 +201,7 @@ namespace Avalonia.Controls
var invert = orient ? 0 : 1;
var calcVal = Math.Abs(invert - logicalPos);
var range = Maximum - Minimum;
var finalValue = calcVal * range;
var finalValue = calcVal * range + Minimum;
Value = IsSnapToTickEnabled ? SnapToTick(finalValue) : finalValue;
}

32
src/Avalonia.Controls/TextBox.cs

@ -347,7 +347,7 @@ namespace Avalonia.Controls
if (IsFocused)
{
DecideCaretVisibility();
_presenter?.ShowCaret();
}
}
@ -364,14 +364,7 @@ namespace Avalonia.Controls
{
SelectAll();
}
else
{
DecideCaretVisibility();
}
}
private void DecideCaretVisibility()
{
_presenter?.ShowCaret();
}
@ -580,15 +573,15 @@ namespace Avalonia.Controls
switch (e.Key)
{
case Key.Left:
MoveHorizontal(-1, hasWholeWordModifiers);
movement = true;
selection = DetectSelection();
MoveHorizontal(-1, hasWholeWordModifiers, selection);
movement = true;
break;
case Key.Right:
MoveHorizontal(1, hasWholeWordModifiers);
movement = true;
selection = DetectSelection();
MoveHorizontal(1, hasWholeWordModifiers, selection);
movement = true;
break;
case Key.Up:
@ -833,13 +826,21 @@ namespace Avalonia.Controls
return result;
}
private void MoveHorizontal(int direction, bool wholeWord)
private void MoveHorizontal(int direction, bool wholeWord, bool isSelecting)
{
var text = Text ?? string.Empty;
var caretIndex = CaretIndex;
if (!wholeWord)
{
if (SelectionStart != SelectionEnd && !isSelecting)
{
var start = Math.Min(SelectionStart, SelectionEnd);
var end = Math.Max(SelectionStart, SelectionEnd);
CaretIndex = direction < 0 ? start : end;
return;
}
var index = caretIndex + direction;
if (index < 0 || index > text.Length)
@ -975,6 +976,7 @@ namespace Avalonia.Controls
{
SelectionStart = 0;
SelectionEnd = Text?.Length ?? 0;
CaretIndex = SelectionEnd;
}
private bool DeleteSelection()
@ -1055,14 +1057,14 @@ namespace Avalonia.Controls
private void SetSelectionForControlBackspace()
{
SelectionStart = CaretIndex;
MoveHorizontal(-1, true);
MoveHorizontal(-1, true, false);
SelectionEnd = CaretIndex;
}
private void SetSelectionForControlDelete()
{
SelectionStart = CaretIndex;
MoveHorizontal(1, true);
MoveHorizontal(1, true, false);
SelectionEnd = CaretIndex;
}

105
src/Avalonia.Controls/ToggleSwitch.cs

@ -0,0 +1,105 @@
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.LogicalTree;
namespace Avalonia.Controls
{
/// <summary>
/// A Toggle Switch control.
/// </summary>
public class ToggleSwitch : ToggleButton
{
static ToggleSwitch()
{
OffContentProperty.Changed.AddClassHandler<ToggleSwitch>((x, e) => x.OffContentChanged(e));
OnContentProperty.Changed.AddClassHandler<ToggleSwitch>((x, e) => x.OnContentChanged(e));
}
/// <summary>
/// Defines the <see cref="OffContent"/> property.
/// </summary>
public static readonly StyledProperty<object> OffContentProperty =
AvaloniaProperty.Register<ToggleSwitch, object>(nameof(OffContent), defaultValue: "Off");
/// <summary>
/// Defines the <see cref="OffContentTemplate"/> property.
/// </summary>
public static readonly StyledProperty<IDataTemplate> OffContentTemplateProperty =
AvaloniaProperty.Register<ToggleSwitch, IDataTemplate>(nameof(OffContentTemplate));
/// <summary>
/// Defines the <see cref="OnContent"/> property.
/// </summary>
public static readonly StyledProperty<object> OnContentProperty =
AvaloniaProperty.Register<ToggleSwitch, object>(nameof(OnContent), defaultValue: "On");
/// <summary>
/// Defines the <see cref="OnContentTemplate"/> property.
/// </summary>
public static readonly StyledProperty<IDataTemplate> OnContentTemplateProperty =
AvaloniaProperty.Register<ToggleSwitch, IDataTemplate>(nameof(OnContentTemplate));
/// <summary>
/// Gets or Sets the Content that is displayed when in the On State.
/// </summary>
public object OnContent
{
get { return GetValue(OnContentProperty); }
set { SetValue(OnContentProperty, value); }
}
/// <summary>
/// Gets or Sets the Content that is displayed when in the Off State.
/// </summary>
public object OffContent
{
get { return GetValue(OffContentProperty); }
set { SetValue(OffContentProperty, value); }
}
/// <summary>
/// Gets or Sets the <see cref="IDataTemplate"/> used to display the <see cref="OffContent"/>.
/// </summary>
public IDataTemplate OffContentTemplate
{
get { return GetValue(OffContentTemplateProperty); }
set { SetValue(OffContentTemplateProperty, value); }
}
/// <summary>
/// Gets or Sets the <see cref="IDataTemplate"/> used to display the <see cref="OnContent"/>.
/// </summary>
public IDataTemplate OnContentTemplate
{
get { return GetValue(OnContentTemplateProperty); }
set { SetValue(OnContentTemplateProperty, value); }
}
private void OffContentChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.OldValue is ILogical oldChild)
{
LogicalChildren.Remove(oldChild);
}
if (e.NewValue is ILogical newChild)
{
LogicalChildren.Add(newChild);
}
}
private void OnContentChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.OldValue is ILogical oldChild)
{
LogicalChildren.Remove(oldChild);
}
if (e.NewValue is ILogical newChild)
{
LogicalChildren.Add(newChild);
}
}
}
}

3
src/Avalonia.Controls/TopLevel.cs

@ -276,9 +276,6 @@ namespace Avalonia.Controls
set { SetValue(AccessText.ShowAccessKeyProperty, value); }
}
/// <inheritdoc/>
Size ILayoutRoot.MaxClientSize => Size.Infinity;
/// <inheritdoc/>
double ILayoutRoot.LayoutScaling => PlatformImpl?.Scaling ?? 1;

95
src/Avalonia.Controls/Window.cs

@ -69,7 +69,7 @@ namespace Avalonia.Controls
/// </summary>
public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot
{
private List<Window> _children = new List<Window>();
private readonly List<(Window child, bool isDialog)> _children = new List<(Window, bool)>();
/// <summary>
/// Defines the <see cref="SizeToContent"/> property.
@ -188,7 +188,7 @@ namespace Avalonia.Controls
impl.Closing = HandleClosing;
impl.GotInputWhenDisabled = OnGotInputWhenDisabled;
impl.WindowStateChanged = HandleWindowStateChanged;
_maxPlatformClientSize = PlatformImpl?.MaxClientSize ?? default(Size);
_maxPlatformClientSize = PlatformImpl?.MaxAutoSizeHint ?? default(Size);
this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x));
PlatformImpl?.ShowTaskbarIcon(ShowInTaskbar);
@ -318,9 +318,6 @@ namespace Avalonia.Controls
/// </summary>
public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) => PlatformImpl?.BeginResizeDrag(edge, e);
/// <inheritdoc/>
Size ILayoutRoot.MaxClientSize => _maxPlatformClientSize;
/// <inheritdoc/>
Type IStyleable.StyleKey => typeof(Window);
@ -376,7 +373,7 @@ namespace Avalonia.Controls
private void CloseInternal()
{
foreach (var child in _children.ToList())
foreach (var (child, _) in _children.ToList())
{
// if we HandleClosing() before then there will be no children.
child.CloseInternal();
@ -399,7 +396,7 @@ namespace Avalonia.Controls
{
bool canClose = true;
foreach (var child in _children.ToList())
foreach (var (child, _) in _children.ToList())
{
if (!child.HandleClosing())
{
@ -472,6 +469,28 @@ namespace Avalonia.Controls
/// The window has already been closed.
/// </exception>
public override void Show()
{
ShowCore(null);
}
/// <summary>
/// Shows the window as a child of <paramref name="parent"/>.
/// </summary>
/// <param name="parent">Window that will be a parent of the shown window.</param>
/// <exception cref="InvalidOperationException">
/// The window has already been closed.
/// </exception>
public void Show(Window parent)
{
if (parent is null)
{
throw new ArgumentNullException(nameof(parent), "Showing a child window requires valid parent.");
}
ShowCore(parent);
}
private void ShowCore(Window parent)
{
if (PlatformImpl == null)
{
@ -483,7 +502,7 @@ namespace Avalonia.Controls
return;
}
this.RaiseEvent(new RoutedEventArgs(WindowOpenedEvent));
RaiseEvent(new RoutedEventArgs(WindowOpenedEvent));
EnsureInitialized();
IsVisible = true;
@ -504,6 +523,14 @@ namespace Avalonia.Controls
using (BeginAutoSizing())
{
if (parent != null)
{
PlatformImpl?.SetParent(parent.PlatformImpl);
}
Owner = parent;
parent?.AddChild(this, false);
PlatformImpl?.Show();
Renderer?.Start();
}
@ -571,9 +598,9 @@ namespace Avalonia.Controls
using (BeginAutoSizing())
{
PlatformImpl.SetParent(owner.PlatformImpl);
PlatformImpl?.SetParent(owner.PlatformImpl);
Owner = owner;
owner.AddChild(this);
owner.AddChild(this, true);
PlatformImpl?.Show();
Renderer?.Start();
@ -598,28 +625,57 @@ namespace Avalonia.Controls
private void UpdateEnabled()
{
PlatformImpl.SetEnabled(_children.Count == 0);
bool isEnabled = true;
foreach (var (_, isDialog) in _children)
{
if (isDialog)
{
isEnabled = false;
break;
}
}
PlatformImpl.SetEnabled(isEnabled);
}
private void AddChild(Window window)
private void AddChild(Window window, bool isDialog)
{
_children.Add(window);
_children.Add((window, isDialog));
UpdateEnabled();
}
private void RemoveChild(Window window)
{
_children.Remove(window);
for (int i = _children.Count - 1; i >= 0; i--)
{
var (child, _) = _children[i];
if (ReferenceEquals(child, window))
{
_children.RemoveAt(i);
}
}
UpdateEnabled();
}
private void OnGotInputWhenDisabled()
{
var firstChild = _children.FirstOrDefault();
Window firstDialogChild = null;
foreach (var (child, isDialog) in _children)
{
if (isDialog)
{
firstDialogChild = child;
break;
}
}
if (firstChild != null)
if (firstDialogChild != null)
{
firstChild.OnGotInputWhenDisabled();
firstDialogChild.OnGotInputWhenDisabled();
}
else
{
@ -663,15 +719,16 @@ namespace Avalonia.Controls
var sizeToContent = SizeToContent;
var clientSize = ClientSize;
var constraint = clientSize;
var maxAutoSize = PlatformImpl?.MaxAutoSizeHint ?? Size.Infinity;
if (sizeToContent.HasFlagCustom(SizeToContent.Width))
{
constraint = constraint.WithWidth(double.PositiveInfinity);
constraint = constraint.WithWidth(maxAutoSize.Width);
}
if (sizeToContent.HasFlagCustom(SizeToContent.Height))
{
constraint = constraint.WithHeight(double.PositiveInfinity);
constraint = constraint.WithHeight(maxAutoSize.Height);
}
var result = base.MeasureOverride(constraint);

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

@ -44,7 +44,7 @@ namespace Avalonia.DesignerSupport.Remote
public IPlatformHandle Handle { get; }
public WindowState WindowState { get; set; }
public Action<WindowState> WindowStateChanged { get; set; }
public Size MaxClientSize { get; } = new Size(4096, 4096);
public Size MaxAutoSizeHint { get; } = new Size(4096, 4096);
public event Action LostFocus
{
add {}

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

@ -19,7 +19,7 @@ namespace Avalonia.DesignerSupport.Remote
public Action Deactivated { get; set; }
public Action Activated { get; set; }
public IPlatformHandle Handle { get; }
public Size MaxClientSize { get; }
public Size MaxAutoSizeHint { get; }
public Size ClientSize { get; }
public double Scaling { get; } = 1.0;
public IEnumerable<object> Surfaces { get; }

5
src/Avalonia.Layout/ILayoutRoot.cs

@ -10,11 +10,6 @@ namespace Avalonia.Layout
/// </summary>
Size ClientSize { get; }
/// <summary>
/// The maximum client size available.
/// </summary>
Size MaxClientSize { get; }
/// <summary>
/// The scaling factor to use in layout.
/// </summary>

3
src/Avalonia.Native/OsxManagedPopupPositionerPopupImplHelper.cs

@ -9,8 +9,7 @@ namespace Avalonia.Native
{
}
public override Point TranslatePoint(Point pt) => pt;
public override Size TranslateSize(Size size) => size;
public override double Scaling => 1;
}
}

2
src/Avalonia.Native/WindowImplBase.cs

@ -336,7 +336,7 @@ namespace Avalonia.Native
_native.BeginMoveDrag();
}
public Size MaxClientSize => Screen.AllScreens.Select(s => s.Bounds.Size.ToSize(s.PixelDensity))
public Size MaxAutoSizeHint => Screen.AllScreens.Select(s => s.Bounds.Size.ToSize(s.PixelDensity))
.OrderByDescending(x => x.Width + x.Height).FirstOrDefault();
public void SetTopmost(bool value)

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

@ -10,7 +10,7 @@
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}">
<ScrollViewer>
<ScrollViewer Classes="menuscroller">
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"

1
src/Avalonia.Themes.Default/DefaultTheme.xaml

@ -52,4 +52,5 @@
<StyleInclude Source="resm:Avalonia.Themes.Default.WindowNotificationManager.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.NotificationCard.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.NativeMenuBar.xaml?assembly=Avalonia.Themes.Default"/>
<StyleInclude Source="resm:Avalonia.Themes.Default.ToggleSwitch.xaml?assembly=Avalonia.Themes.Default"/>
</Styles>

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

@ -113,7 +113,7 @@
<Border Background="{TemplateBinding Background}"
BorderBrush="{DynamicResource ThemeBorderMidBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ScrollViewer>
<ScrollViewer Classes="menuscroller">
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"

150
src/Avalonia.Themes.Default/ScrollViewer.xaml

@ -1,47 +1,105 @@
<Style xmlns="https://github.com/avaloniaui" Selector="ScrollViewer">
<Setter Property="Background"
Value="Transparent" />
<Setter Property="Template">
<ControlTemplate>
<Grid ColumnDefinitions="*,Auto" RowDefinitions="*,Auto">
<ScrollContentPresenter Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
CanHorizontallyScroll="{TemplateBinding CanHorizontallyScroll}"
CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}"
Content="{TemplateBinding Content}"
Extent="{TemplateBinding Extent, Mode=TwoWay}"
Margin="{TemplateBinding Padding}"
Offset="{TemplateBinding Offset, Mode=TwoWay}"
Viewport="{TemplateBinding Viewport, Mode=TwoWay}">
<ScrollContentPresenter.GestureRecognizers>
<ScrollGestureRecognizer
CanHorizontallyScroll="{TemplateBinding CanHorizontallyScroll}"
CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}"
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:Avalonia.Controls.Converters;assembly=Avalonia.Controls">
<Style Selector="ScrollViewer">
<Setter Property="Background"
Value="Transparent" />
<Setter Property="Template">
<ControlTemplate>
<Grid ColumnDefinitions="*,Auto" RowDefinitions="*,Auto">
<ScrollContentPresenter Name="PART_ContentPresenter"
Background="{TemplateBinding Background}"
CanHorizontallyScroll="{TemplateBinding CanHorizontallyScroll}"
CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}"
Content="{TemplateBinding Content}"
Extent="{TemplateBinding Extent, Mode=TwoWay}"
Margin="{TemplateBinding Padding}"
Offset="{TemplateBinding Offset, Mode=TwoWay}"
Viewport="{TemplateBinding Viewport, Mode=TwoWay}">
<ScrollContentPresenter.GestureRecognizers>
<ScrollGestureRecognizer
CanHorizontallyScroll="{TemplateBinding CanHorizontallyScroll}"
CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}"
/>
</ScrollContentPresenter.GestureRecognizers>
</ScrollContentPresenter>
<ScrollBar Name="horizontalScrollBar"
Orientation="Horizontal"
LargeChange="{Binding LargeChange.Width, RelativeSource={RelativeSource TemplatedParent}}"
SmallChange="{Binding SmallChange.Width, RelativeSource={RelativeSource TemplatedParent}}"
Maximum="{TemplateBinding HorizontalScrollBarMaximum}"
Value="{TemplateBinding HorizontalScrollBarValue, Mode=TwoWay}"
ViewportSize="{TemplateBinding HorizontalScrollBarViewportSize}"
Visibility="{TemplateBinding HorizontalScrollBarVisibility}"
Grid.Row="1"
Focusable="False"/>
<ScrollBar Name="verticalScrollBar"
Orientation="Vertical"
LargeChange="{Binding LargeChange.Height, RelativeSource={RelativeSource TemplatedParent}}"
SmallChange="{Binding SmallChange.Height, RelativeSource={RelativeSource TemplatedParent}}"
Maximum="{TemplateBinding VerticalScrollBarMaximum}"
Value="{TemplateBinding VerticalScrollBarValue, Mode=TwoWay}"
ViewportSize="{TemplateBinding VerticalScrollBarViewportSize}"
Visibility="{TemplateBinding VerticalScrollBarVisibility}"
Grid.Column="1"
Focusable="False"/>
<Panel Grid.Row="1" Grid.Column="1" Background="{DynamicResource ThemeControlMidBrush}"/>
</Grid>
</ControlTemplate>
</Setter>
</Style>
</ScrollContentPresenter.GestureRecognizers>
</ScrollContentPresenter>
<ScrollBar Name="horizontalScrollBar"
Orientation="Horizontal"
LargeChange="{Binding LargeChange.Width, RelativeSource={RelativeSource TemplatedParent}}"
SmallChange="{Binding SmallChange.Width, RelativeSource={RelativeSource TemplatedParent}}"
Maximum="{TemplateBinding HorizontalScrollBarMaximum}"
Value="{TemplateBinding HorizontalScrollBarValue, Mode=TwoWay}"
ViewportSize="{TemplateBinding HorizontalScrollBarViewportSize}"
Visibility="{TemplateBinding HorizontalScrollBarVisibility}"
Grid.Row="1"
Focusable="False"/>
<ScrollBar Name="verticalScrollBar"
Orientation="Vertical"
LargeChange="{Binding LargeChange.Height, RelativeSource={RelativeSource TemplatedParent}}"
SmallChange="{Binding SmallChange.Height, RelativeSource={RelativeSource TemplatedParent}}"
Maximum="{TemplateBinding VerticalScrollBarMaximum}"
Value="{TemplateBinding VerticalScrollBarValue, Mode=TwoWay}"
ViewportSize="{TemplateBinding VerticalScrollBarViewportSize}"
Visibility="{TemplateBinding VerticalScrollBarVisibility}"
Grid.Column="1"
Focusable="False"/>
<Panel Grid.Row="1" Grid.Column="1" Background="{DynamicResource ThemeControlMidBrush}"/>
</Grid>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="ScrollViewer.menuscroller">
<Setter Property="Template">
<ControlTemplate>
<DockPanel>
<RepeatButton DockPanel.Dock="Top"
BorderThickness="0"
Background="Transparent"
Command="{Binding LineUp, RelativeSource={RelativeSource TemplatedParent}}">
<RepeatButton.IsVisible>
<MultiBinding Converter="{x:Static converters:MenuScrollingVisibilityConverter.Instance}"
ConverterParameter="0">
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="VerticalScrollBarVisibility"/>
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Offset.Y"/>
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Extent.Height"/>
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Viewport.Height"/>
</MultiBinding>
</RepeatButton.IsVisible>
<Path Data="M 0 4 L 8 4 L 4 0 Z"/>
</RepeatButton>
<RepeatButton DockPanel.Dock="Bottom"
BorderThickness="0"
Background="Transparent"
Command="{Binding LineDown, RelativeSource={RelativeSource TemplatedParent}}">
<RepeatButton.IsVisible>
<MultiBinding Converter="{x:Static converters:MenuScrollingVisibilityConverter.Instance}"
ConverterParameter="100">
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="VerticalScrollBarVisibility"/>
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Offset.Y"/>
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Extent.Height"/>
<Binding RelativeSource="{RelativeSource TemplatedParent}" Path="Viewport.Height"/>
</MultiBinding>
</RepeatButton.IsVisible>
<Path Data="M 0 0 L 4 4 L 8 0 Z"/>
</RepeatButton>
<ScrollContentPresenter Name="PART_ContentPresenter"
CanHorizontallyScroll="{TemplateBinding CanHorizontallyScroll}"
CanVerticallyScroll="{TemplateBinding CanVerticallyScroll}"
Content="{TemplateBinding Content}"
Extent="{TemplateBinding Extent, Mode=TwoWay}"
Margin="{TemplateBinding Padding}"
Offset="{TemplateBinding Offset, Mode=TwoWay}"
Viewport="{TemplateBinding Viewport, Mode=TwoWay}"/>
</DockPanel>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="ScrollViewer.menuscroller /template/ RepeatButton > Path">
<Setter Property="Fill" Value="{DynamicResource ThemeForegroundLowBrush}" />
</Style>
<Style Selector="ScrollViewer.menuscroller /template/ RepeatButton:pointerover > Path">
<Setter Property="Fill" Value="{DynamicResource ThemeAccentBrush}" />
</Style>
</Styles>

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

@ -46,7 +46,7 @@
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Border Name="TrackBackground" Grid.Column="1" Width="4" Margin="0,6" HorizontalAlignment="Center"/>
<Track Name="PART_Track" Grid.Column="1" Orientation="Vertical" IsDirectionReversed="True">
<Track Name="PART_Track" Grid.Column="1" Orientation="Vertical">
<Track.DecreaseButton>
<RepeatButton Name="PART_DecreaseButton"
Classes="repeattrack" />

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

@ -0,0 +1,294 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=netstandard">
<Styles.Resources>
<Thickness x:Key="ToggleSwitchTopHeaderMargin">0,0,0,6</Thickness>
<x:Double x:Key="ToggleSwitchPreContentMargin">6</x:Double>
<x:Double x:Key="ToggleSwitchPostContentMargin">6</x:Double>
<x:Double x:Key="ToggleSwitchThemeMinWidth">154</x:Double>
<x:Double x:Key="KnobOnPosition">20</x:Double>
<x:Double x:Key="KnobOffPosition">0</x:Double>
</Styles.Resources>
<Design.PreviewWith>
<StackPanel Margin="20" Width="250" Spacing="24" >
<StackPanel Spacing="12" >
<TextBlock
Text="Automatic updates"
Classes="h1"/>
<TextBlock
Text="Updates will be automaticly Downloaded and installed shile the computer is shutting down or restarting"
TextWrapping="Wrap"/>
<ToggleSwitch HorizontalContentAlignment="Left"
Content="Enable automatic Updates?"
OffContent="Uit"
OnContent="Aan"
VerticalAlignment="Bottom"/>
</StackPanel>
<StackPanel Spacing="12">
<TextBlock
Text="Previewer"
Classes="h1"/>
<TextBlock
Text="The previewer Shows a preview off your code, this could slow down your system"
TextWrapping="Wrap"/>
<ToggleSwitch
Content="Previewer"
IsChecked="True" />
</StackPanel>
</StackPanel>
</Design.PreviewWith>
<Style Selector="ToggleSwitch">
<Setter Property="Foreground" Value="{DynamicResource ToggleSwitchContentForeground}" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="FontFamily" Value="{DynamicResource ContentControlThemeFontFamily}" />
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="Template">
<ControlTemplate>
<Grid Background="{TemplateBinding Background}"
RowDefinitions="Auto,*">
<ContentPresenter x:Name="PART_ContentPresenter"
Grid.Row="0"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Margin="{DynamicResource ToggleSwitchTopHeaderMargin}"
VerticalAlignment="Top"/>
<Grid Grid.Row="1"
MinWidth="{StaticResource ToggleSwitchThemeMinWidth}"
HorizontalAlignment="Left"
VerticalAlignment="Top">
<Grid.RowDefinitions>
<RowDefinition Height="{DynamicResource ToggleSwitchPreContentMargin}" />
<RowDefinition Height="Auto" />
<RowDefinition Height="{DynamicResource ToggleSwitchPostContentMargin}" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="12" MaxWidth="12" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid x:Name="SwitchAreaGrid"
Grid.RowSpan="3"
Grid.ColumnSpan="3"
TemplatedControl.IsTemplateFocusTarget="True"
Margin="0,5" />
<ContentPresenter x:Name="PART_OffContentPresenter"
Grid.RowSpan="3"
Grid.Column="2"
Content="{TemplateBinding OffContent}"
ContentTemplate="{TemplateBinding OffContentTemplate}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
<ContentPresenter x:Name="PART_OnContentPresenter"
Grid.RowSpan="3"
Grid.Column="2"
Content="{TemplateBinding OnContent}"
ContentTemplate="{TemplateBinding OnContentTemplate}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
<Border x:Name="OuterBorder"
Grid.Row="1"
Height="20"
Width="40"
CornerRadius="10"
BorderThickness="{DynamicResource ToggleSwitchOuterBorderStrokeThickness}" />
<Border x:Name="SwitchKnobBounds"
Grid.Row="1"
Height="20"
Width="40"
CornerRadius="10"
BorderThickness="{DynamicResource ToggleSwitchOnStrokeThickness}"/>
<Canvas x:Name="SwitchKnob" Grid.Row="1"
HorizontalAlignment="Left"
Width="20" Height="20">
<Grid x:Name="MovingKnobs"
Width="20" Height="20">
<Ellipse x:Name="SwitchKnobOn"
Width="10" Height="10" />
<Ellipse x:Name="SwitchKnobOff"
Width="10" Height="10" />
</Grid>
</Canvas>
</Grid>
</Grid>
</ControlTemplate>
</Setter>
</Style>
<!-- NormalState -->
<Style Selector="ToggleSwitch /template/ Grid#SwitchAreaGrid">
<Setter Property="Background" Value="{DynamicResource ToggleSwitchContainerBackground}"/>
</Style>
<Style Selector="ToggleSwitch /template/ Border#OuterBorder">
<Setter Property="Background" Value="{DynamicResource ToggleSwitchFillOff}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ToggleSwitchStrokeOff}"/>
</Style>
<Style Selector="ToggleSwitch /template/ Border#SwitchKnobBounds">
<Setter Property="Background" Value="{DynamicResource ToggleSwitchFillOn}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ToggleSwitchStrokeOn}"/>
</Style>
<Style Selector="ToggleSwitch /template/ Ellipse#SwitchKnobOn">
<Setter Property="Fill" Value="{DynamicResource ToggleSwitchKnobFillOn}"/>
<Setter Property="Stroke" Value="{DynamicResource ToggleSwitchKnobStrokeOn}"/>
</Style>
<Style Selector="ToggleSwitch /template/ Ellipse#SwitchKnobOff">
<Setter Property="Fill" Value="{DynamicResource ToggleSwitchKnobFillOff}"/>
<Setter Property="Stroke" Value="{DynamicResource ToggleSwitchKnobStrokeOff}"/>
</Style>
<Style Selector="ToggleSwitch /template/ Grid#MovingKnobs">
<Setter Property="Canvas.Left" Value="{DynamicResource KnobOffPosition}"/>
<Setter Property="Transitions">
<Transitions>
<DoubleTransition Property="Canvas.Left" Duration="0:0:0.2" Easing="CubicEaseOut"/>
</Transitions>
</Setter>
</Style>
<!-- PointerOverState -->
<Style Selector="ToggleSwitch:pointerover /template/ Border#OuterBorder">
<Setter Property="BorderBrush" Value="{DynamicResource ToggleSwitchStrokeOffPointerOver}"/>
<Setter Property="Background" Value="{DynamicResource ToggleSwitchFillOffPointerOver}"/>
</Style>
<Style Selector="ToggleSwitch:pointerover /template/ Ellipse#SwitchKnobOff">
<Setter Property="Fill" Value="{DynamicResource ToggleSwitchKnobFillOffPointerOver}"/>
</Style>
<Style Selector="ToggleSwitch:pointerover /template/ Ellipse#SwitchKnobOn">
<Setter Property="Fill" Value="{DynamicResource ToggleSwitchKnobFillOnPointerOver}"/>
</Style>
<Style Selector="ToggleSwitch:pointerover /template/ Border#SwitchKnobBounds">
<Setter Property="Background" Value="{DynamicResource ToggleSwitchFillOnPointerOver}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ToggleSwitchStrokeOnPointerOver}"/>
</Style>
<Style Selector="ToggleSwitch:pointerover /template/ Grid#SwitchAreaGrid">
<Setter Property="Background" Value="{DynamicResource ToggleSwitchContainerBackgroundPointerOver}"/>
</Style>
<!-- PressedState -->
<Style Selector="ToggleSwitch:pressed /template/ Border#OuterBorder">
<Setter Property="BorderBrush" Value="{DynamicResource ToggleSwitchStrokeOffPressed}"/>
<Setter Property="Background" Value="{DynamicResource ToggleSwitchFillOffPressed}"/>
</Style>
<Style Selector="ToggleSwitch:pressed /template/ Border#SwitchKnobBounds">
<Setter Property="Background" Value="{DynamicResource ToggleSwitchFillOnPressed}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ToggleSwitchStrokeOnPressed}"/>
</Style>
<Style Selector="ToggleSwitch:pressed /template/ Ellipse#SwitchKnobOff">
<Setter Property="Fill" Value="{DynamicResource ToggleSwitchKnobFillOffPressed}"/>
</Style>
<Style Selector="ToggleSwitch:pressed /template/ Ellipse#SwitchKnobOn">
<Setter Property="Fill" Value="{DynamicResource ToggleSwitchKnobFillOnPressed}"/>
</Style>
<Style Selector="ToggleSwitch:pressed /template/ Grid#SwitchAreaGrid">
<Setter Property="Background" Value="{DynamicResource ToggleSwitchContainerBackgroundPressed}"/>
</Style>
<!-- DisabledState -->
<Style Selector="ToggleSwitch:disabled">
<Setter Property="Foreground" Value="{DynamicResource ToggleSwitchHeaderForegroundDisabled}"/>
</Style>
<Style Selector="ToggleSwitch:disabled /template/ Border#OuterBorder">
<Setter Property="BorderBrush" Value="{DynamicResource ToggleSwitchStrokeOffDisabled}"/>
<Setter Property="Background" Value="{DynamicResource ToggleSwitchFillOffPressed}"/>
</Style>
<Style Selector="ToggleSwitch:disabled /template/ Ellipse#SwitchKnobOff">
<Setter Property="Fill" Value="{DynamicResource ToggleSwitchKnobFillOffDisabled}"/>
</Style>
<Style Selector="ToggleSwitch:disabled /template/ Ellipse#SwitchKnobOn">
<Setter Property="Fill" Value="{DynamicResource ToggleSwitchKnobFillOnDisabled}"/>
</Style>
<Style Selector="ToggleSwitch:disabled /template/ Border#SwitchKnobBounds">
<Setter Property="Background" Value="{DynamicResource ToggleSwitchFillOnDisabled}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ToggleSwitchStrokeOnDisabled}"/>
</Style>
<!-- CheckedState -->
<Style Selector="ToggleSwitch:checked /template/ Grid#MovingKnobs">
<Setter Property="Canvas.Left" Value="{DynamicResource KnobOnPosition}"/>
</Style>
<Style Selector="ToggleSwitch:checked /template/ Border#OuterBorder">
<Setter Property="Opacity" Value="0"/>
</Style>
<Style Selector="ToggleSwitch:checked /template/ Ellipse#SwitchKnobOff">
<Setter Property="Opacity" Value="0"/>
</Style>
<Style Selector="ToggleSwitch:checked /template/ Border#SwitchKnobBounds">
<Setter Property="Opacity" Value="1"/>
</Style>
<Style Selector="ToggleSwitch:checked /template/ Ellipse#SwitchKnobOn">
<Setter Property="Opacity" Value="1"/>
</Style>
<Style Selector="ToggleSwitch:checked /template/ ContentPresenter#PART_OffContentPresenter">
<Setter Property="Opacity" Value="0"/>
</Style>
<Style Selector="ToggleSwitch:checked /template/ ContentPresenter#PART_OnContentPresenter">
<Setter Property="Opacity" Value="1"/>
</Style>
<!--UncheckedState -->
<Style Selector="ToggleSwitch:unchecked /template/ Grid#MovingKnobs">
<Setter Property="Canvas.Left" Value="{DynamicResource KnobOffPosition}"/>
</Style>
<Style Selector="ToggleSwitch:unchecked /template/ Border#OuterBorder">
<Setter Property="Opacity" Value="1"/>
</Style>
<Style Selector="ToggleSwitch:unchecked /template/ Ellipse#SwitchKnobOff">
<Setter Property="Opacity" Value="1"/>
</Style>
<Style Selector="ToggleSwitch:unchecked /template/ Ellipse#SwitchKnobOn">
<Setter Property="Opacity" Value="0"/>
</Style>
<Style Selector="ToggleSwitch:unchecked /template/ Border#SwitchKnobBounds">
<Setter Property="Opacity" Value="0"/>
</Style>
<Style Selector="ToggleSwitch:unchecked /template/ ContentPresenter#PART_OffContentPresenter">
<Setter Property="Opacity" Value="1"/>
</Style>
<Style Selector="ToggleSwitch:unchecked /template/ ContentPresenter#PART_OnContentPresenter">
<Setter Property="Opacity" Value="0"/>
</Style>
</Styles>

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

@ -138,6 +138,9 @@
<SolidColorBrush x:Key="SystemControlTransparentBrush" Color="Transparent" />
<SolidColorBrush x:Key="SystemControlErrorTextForegroundBrush" Color="{StaticResource SystemErrorTextColor}" />
<SolidColorBrush x:Key="SystemControlTransientBorderBrush" Color="#000000" Opacity="0.36" />
<SolidColorBrush x:Key="SystemControlHighlightListLowRevealBackgroundBrush" Color="{StaticResource SystemListMediumColor}" />
<SolidColorBrush x:Key="SystemControlHighlightListMediumRevealBackgroundBrush" Color="{StaticResource SystemListMediumColor}" />
<SolidColorBrush x:Key="SystemControlHighlightAccentRevealBackgroundBrush" Color="{StaticResource SystemAccentColor}" />
<!-- TODO implement AcrylicBrush -->
<!--<AcrylicBrush x:Key="SystemControlTransientBackgroundBrush" BackgroundSource="HostBackdrop" TintColor="{StaticResource SystemChromeAltHighColor}" TintOpacity="0.8" FallbackColor="{StaticResource SystemChromeMediumLowColor}" />-->
<SolidColorBrush x:Key="SystemControlTransientBackgroundBrush" Color="{StaticResource SystemChromeMediumLowColor}" />

3
src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml

@ -138,6 +138,9 @@
<SolidColorBrush x:Key="SystemControlTransparentBrush" Color="Transparent" />
<SolidColorBrush x:Key="SystemControlErrorTextForegroundBrush" Color="{StaticResource SystemErrorTextColor}" />
<SolidColorBrush x:Key="SystemControlTransientBorderBrush" Color="#000000" Opacity="0.14" />
<SolidColorBrush x:Key="SystemControlHighlightListLowRevealBackgroundBrush" Color="{StaticResource SystemListMediumColor}" />
<SolidColorBrush x:Key="SystemControlHighlightListMediumRevealBackgroundBrush" Color="{StaticResource SystemListMediumColor}" />
<SolidColorBrush x:Key="SystemControlHighlightAccentRevealBackgroundBrush" Color="{StaticResource SystemAccentColor}" />
<!-- TODO implement AcrylicBrush -->
<!--<AcrylicBrush x:Key="SystemControlTransientBackgroundBrush" BackgroundSource="HostBackdrop" TintColor="{StaticResource SystemChromeAltHighColor}" TintOpacity="0.8" FallbackColor="{StaticResource SystemChromeMediumLowColor}" />-->
<SolidColorBrush x:Key="SystemControlTransientBackgroundBrush" Color="{StaticResource SystemChromeMediumLowColor}" />

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

@ -1,5 +1,5 @@
<Style xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
<Style xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=netstandard">
<Style.Resources>
<!-- Resources for Button.xaml -->
@ -62,7 +62,7 @@
<SolidColorBrush x:Key="RepeatButtonPointerOverForegroundThemeBrush" Color="#FFFFFFFF" />
<SolidColorBrush x:Key="RepeatButtonPressedBackgroundThemeBrush" Color="#FFFFFFFF" />
<SolidColorBrush x:Key="RepeatButtonPressedForegroundThemeBrush" Color="#FF000000" />
<!-- Resources for ToggleButton.xaml -->
<Thickness x:Key="ToggleButtonBorderThemeThickness">1</Thickness>
<StaticResource x:Key="ToggleButtonBackground" ResourceKey="SystemControlBackgroundBaseLowBrush" />
@ -182,8 +182,8 @@
<StaticResource x:Key="ComboBoxDropDownGlyphForeground" ResourceKey="SystemControlForegroundBaseMediumHighBrush" />
<StaticResource x:Key="ComboBoxDropDownGlyphForegroundDisabled" ResourceKey="SystemControlDisabledBaseMediumLowBrush" />
<StaticResource x:Key="ComboBoxDropDownGlyphForegroundFocused" ResourceKey="SystemControlHighlightAltBaseMediumHighBrush" />
<StaticResource x:Key="ComboBoxDropDownGlyphForegroundFocusedPressed" ResourceKey="SystemControlHighlightAltBaseMediumHighBrush" />
<StaticResource x:Key="ComboBoxDropDownForeground" ResourceKey="SystemControlForegroundBaseHighBrush" />
<StaticResource x:Key="ComboBoxDropDownGlyphForegroundFocusedPressed" ResourceKey="SystemControlHighlightAltBaseMediumHighBrush" />
<StaticResource x:Key="ComboBoxDropDownForeground" ResourceKey="SystemControlForegroundBaseHighBrush" />
<StaticResource x:Key="ComboBoxDropDownBackground" ResourceKey="SystemControlTransientBackgroundBrush" />
<StaticResource x:Key="ComboBoxDropDownBorderBrush" ResourceKey="SystemControlTransientBorderBrush" />
@ -250,6 +250,92 @@
<SolidColorBrush x:Key="ListBoxItemSelectedForegroundThemeBrush" Color="White" />
<SolidColorBrush x:Key="ListBoxItemSelectedPointerOverBackgroundThemeBrush" Color="#FF5F37BE" />
<!-- Resources for MenuFlyout.xaml (Menu, ContextMenu, etc) -->
<x:Double x:Key="MenuFlyoutSeparatorThemeHeight">1</x:Double>
<x:Double x:Key="MenuFlyoutThemeMinHeight">32</x:Double>
<Thickness x:Key="MenuFlyoutPresenterThemePadding">0,0</Thickness>
<!--<Thickness x:Key="MenuFlyoutItemCheckGlyphMargin">12,11,0,13</Thickness>-->
<Thickness x:Key="MenuFlyoutItemChevronMargin">12,0,0,0</Thickness>
<!--<Thickness x:Key="MenuFlyoutItemPlaceholderThemeThickness">28,0,0,0</Thickness>-->
<Thickness x:Key="MenuFlyoutSeparatorThemePadding">12,4,12,4</Thickness>
<StaticResource x:Key="MenuFlyoutItemBackground" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="MenuFlyoutItemBackgroundPointerOver" ResourceKey="SystemControlHighlightListLowBrush" />
<StaticResource x:Key="MenuFlyoutItemBackgroundPressed" ResourceKey="SystemControlHighlightListMediumBrush" />
<StaticResource x:Key="MenuFlyoutItemBackgroundDisabled" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="MenuFlyoutItemForeground" ResourceKey="SystemControlForegroundBaseHighBrush" />
<StaticResource x:Key="MenuFlyoutItemForegroundPointerOver" ResourceKey="SystemControlHighlightAltBaseHighBrush" />
<StaticResource x:Key="MenuFlyoutItemForegroundPressed" ResourceKey="SystemControlHighlightAltBaseHighBrush" />
<StaticResource x:Key="MenuFlyoutItemForegroundDisabled" ResourceKey="SystemControlDisabledBaseMediumLowBrush" />
<!--<StaticResource x:Key="MenuFlyoutSubItemBackground" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="MenuFlyoutSubItemBackgroundPointerOver" ResourceKey="SystemControlHighlightListLowBrush" />
<StaticResource x:Key="MenuFlyoutSubItemBackgroundPressed" ResourceKey="SystemControlHighlightListAccentHighBrush" />
<StaticResource x:Key="MenuFlyoutSubItemBackgroundSubMenuOpened" ResourceKey="SystemControlHighlightListLowBrush" />
<StaticResource x:Key="MenuFlyoutSubItemBackgroundDisabled" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="MenuFlyoutSubItemForeground" ResourceKey="SystemControlForegroundBaseHighBrush" />
<StaticResource x:Key="MenuFlyoutSubItemForegroundPointerOver" ResourceKey="SystemControlHighlightAltBaseHighBrush" />
<StaticResource x:Key="MenuFlyoutSubItemForegroundPressed" ResourceKey="SystemControlHighlightAltBaseHighBrush" />
<StaticResource x:Key="MenuFlyoutSubItemForegroundSubMenuOpened" ResourceKey="SystemControlHighlightAltBaseHighBrush" />
<StaticResource x:Key="MenuFlyoutSubItemForegroundDisabled" ResourceKey="SystemControlDisabledBaseMediumLowBrush" />-->
<StaticResource x:Key="MenuFlyoutSubItemChevron" ResourceKey="SystemControlForegroundBaseMediumHighBrush" />
<StaticResource x:Key="MenuFlyoutSubItemChevronPointerOver" ResourceKey="SystemControlHighlightAltBaseHighBrush" />
<StaticResource x:Key="MenuFlyoutSubItemChevronPressed" ResourceKey="SystemControlHighlightAltBaseHighBrush" />
<StaticResource x:Key="MenuFlyoutSubItemChevronSubMenuOpened" ResourceKey="SystemControlHighlightAltBaseHighBrush" />
<StaticResource x:Key="MenuFlyoutSubItemChevronDisabled" ResourceKey="SystemControlDisabledBaseMediumLowBrush" />
<StaticResource x:Key="MenuFlyoutLightDismissOverlayBackground" ResourceKey="SystemControlPageBackgroundMediumAltMediumBrush" />
<!--<SolidColorBrush x:Key="MenuFlyoutItemFocusedBackgroundThemeBrush" Color="#FF212121" />
<SolidColorBrush x:Key="MenuFlyoutItemFocusedForegroundThemeBrush" Color="#FFFFFFFF" />
<SolidColorBrush x:Key="MenuFlyoutItemDisabledForegroundThemeBrush" Color="#66FFFFFF" />
<SolidColorBrush x:Key="MenuFlyoutItemPointerOverBackgroundThemeBrush" Color="#FF212121" />
<SolidColorBrush x:Key="MenuFlyoutItemPointerOverForegroundThemeBrush" Color="#FFFFFFFF" />
<SolidColorBrush x:Key="MenuFlyoutItemPressedBackgroundThemeBrush" Color="#FFFFFFFF" />
<SolidColorBrush x:Key="MenuFlyoutItemPressedForegroundThemeBrush" Color="#FF000000" />-->
<!--<SolidColorBrush x:Key="MenuFlyoutSeparatorThemeBrush" Color="#FF7A7A7A" />-->
<!--<Thickness x:Key="MenuFlyoutItemDoublePlaceholderThemeThickness">56,0,0,0</Thickness>-->
<StaticResource x:Key="MenuFlyoutItemKeyboardAcceleratorTextForeground" ResourceKey="SystemControlForegroundBaseMediumBrush" />
<StaticResource x:Key="MenuFlyoutItemKeyboardAcceleratorTextForegroundPointerOver" ResourceKey="SystemControlHighlightAltBaseMediumBrush" />
<StaticResource x:Key="MenuFlyoutItemKeyboardAcceleratorTextForegroundPressed" ResourceKey="SystemControlHighlightAltBaseMediumBrush" />
<StaticResource x:Key="MenuFlyoutItemKeyboardAcceleratorTextForegroundDisabled" ResourceKey="SystemControlDisabledBaseMediumLowBrush" />
<Thickness x:Key="MenuFlyoutItemThemePadding">11,9,11,10</Thickness>
<Thickness x:Key="MenuFlyoutItemThemePaddingNarrow">11,4,11,7</Thickness>
<StaticResource x:Key="MenuFlyoutPresenterBackground" ResourceKey="SystemControlTransientBackgroundBrush" />
<StaticResource x:Key="MenuFlyoutPresenterBorderBrush" ResourceKey="SystemControlTransientBorderBrush" />
<Thickness x:Key="MenuFlyoutPresenterBorderThemeThickness">1</Thickness>
<!-- Resources for MenuFlyoutItem -->
<!--
<StaticResource x:Key="MenuFlyoutItemRevealBackground" ResourceKey="SystemControlTransparentRevealBackgroundBrush" />
<StaticResource x:Key="MenuFlyoutItemRevealBackgroundPointerOver" ResourceKey="SystemControlHighlightListLowRevealBackgroundBrush" />
<StaticResource x:Key="MenuFlyoutItemRevealBackgroundPressed" ResourceKey="SystemControlHighlightListMediumRevealBackgroundBrush" />
<StaticResource x:Key="MenuFlyoutItemRevealBackgroundDisabled" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="MenuFlyoutItemRevealBorderBrush" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="MenuFlyoutItemRevealBorderBrushPressed" ResourceKey="SystemControlTransparentRevealBorderBrush" />
<StaticResource x:Key="MenuFlyoutItemRevealBorderBrushPointerOver" ResourceKey="SystemControlTransparentRevealBorderBrush" />
<StaticResource x:Key="MenuFlyoutItemRevealBorderBrushDisabled" ResourceKey="SystemControlTransparentBrush" />
-->
<!-- Resources for ToggleMenuFlyoutItem -->
<!--
<StaticResource x:Key="ToggleMenuFlyoutItemRevealBackground" ResourceKey="SystemControlTransparentRevealBackgroundBrush" />
<StaticResource x:Key="ToggleMenuFlyoutItemRevealBackgroundPointerOver" ResourceKey="SystemControlHighlightListLowRevealBackgroundBrush" />
<StaticResource x:Key="ToggleMenuFlyoutItemRevealBackgroundPressed" ResourceKey="SystemControlHighlightListMediumRevealBackgroundBrush" />
<StaticResource x:Key="ToggleMenuFlyoutItemRevealBackgroundDisabled" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="ToggleMenuFlyoutItemRevealBorderBrush" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="ToggleMenuFlyoutItemRevealBorderBrushPressed" ResourceKey="SystemControlTransparentRevealBorderBrush" />
<StaticResource x:Key="ToggleMenuFlyoutItemRevealBorderBrushPointerOver" ResourceKey="SystemControlTransparentRevealBorderBrush" />
<StaticResource x:Key="ToggleMenuFlyoutItemRevealBorderBrushDisabled" ResourceKey="SystemControlTransparentBrush" />
-->
<!-- Resources for MenuFlyoutSubItem -->
<!--
<StaticResource x:Key="MenuFlyoutSubItemRevealBackground" ResourceKey="SystemControlTransparentRevealBackgroundBrush" />
<StaticResource x:Key="MenuFlyoutSubItemRevealBackgroundPointerOver" ResourceKey="SystemControlHighlightListLowRevealBackgroundBrush" />
<StaticResource x:Key="MenuFlyoutSubItemRevealBackgroundPressed" ResourceKey="SystemControlHighlightAccentRevealBackgroundBrush" />
<StaticResource x:Key="MenuFlyoutSubItemRevealBackgroundSubMenuOpened" ResourceKey="SystemControlHighlightListLowRevealBackgroundBrush" />
<StaticResource x:Key="MenuFlyoutSubItemRevealBackgroundDisabled" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="MenuFlyoutSubItemRevealBorderBrush" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="MenuFlyoutSubItemRevealBorderBrushPressed" ResourceKey="SystemControlTransparentRevealBorderBrush" />
<StaticResource x:Key="MenuFlyoutSubItemRevealBorderBrushPointerOver" ResourceKey="SystemControlTransparentRevealBorderBrush" />
<StaticResource x:Key="MenuFlyoutSubItemRevealBorderBrushSubMenuOpened" ResourceKey="SystemControlTransparentRevealBorderBrush" />
<StaticResource x:Key="MenuFlyoutSubItemRevealBorderBrushDisabled" ResourceKey="SystemControlTransparentBrush" />-->
<!--<Thickness x:Key="LanguageSwitcherMenuFlyoutItemPlaceholderThemeThickness">44,0,0,0</Thickness>-->
<!-- Resources for ProgressBar.xaml -->
<x:Double x:Key="ProgressBarIndicatorPauseOpacity">0.6</x:Double>
<x:Double x:Key="ProgressBarThemeMinHeight">4</x:Double>
@ -460,7 +546,7 @@
<SolidColorBrush x:Key="RadioButtonPressedBorderThemeBrush" Color="#FFFFFFFF" />
<SolidColorBrush x:Key="RadioButtonPressedForegroundThemeBrush" Color="#FF000000" />
<SolidColorBrush x:Key="RadioButtonContentPointerOverForegroundThemeBrush" Color="{DynamicResource SystemColorHighlightTextColor}" />
<!-- Resources for Slider.xaml -->
<x:Double x:Key="SliderOutsideTickBarThemeHeight">4</x:Double>
<x:Double x:Key="SliderTrackThemeHeight">2</x:Double>
@ -511,6 +597,65 @@
<SolidColorBrush x:Key="SliderTrackPressedBackgroundThemeBrush" Color="#59FFFFFF" />
<SolidColorBrush x:Key="SliderHeaderForegroundThemeBrush" Color="#FFFFFFFF" />
<!--ToggleSwitch-->
<Thickness x:Key="ToggleSwitchOnStrokeThickness">0</Thickness>
<Thickness x:Key="ToggleSwitchOuterBorderStrokeThickness">1</Thickness>
<StaticResource x:Key="ToggleSwitchContentForeground" ResourceKey="SystemControlForegroundBaseHighBrush" />
<StaticResource x:Key="ToggleSwitchContentForegroundDisabled" ResourceKey="SystemControlDisabledBaseMediumLowBrush" />
<StaticResource x:Key="ToggleSwitchHeaderForeground" ResourceKey="SystemControlForegroundBaseHighBrush" />
<StaticResource x:Key="ToggleSwitchHeaderForegroundDisabled" ResourceKey="SystemControlDisabledBaseMediumLowBrush" />
<StaticResource x:Key="ToggleSwitchContainerBackground" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="ToggleSwitchContainerBackgroundPointerOver" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="ToggleSwitchContainerBackgroundPressed" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="ToggleSwitchContainerBackgroundDisabled" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="ToggleSwitchFillOff" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="ToggleSwitchFillOffPointerOver" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="ToggleSwitchFillOffPressed" ResourceKey="SystemControlHighlightBaseMediumLowBrush" />
<StaticResource x:Key="ToggleSwitchFillOffDisabled" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="ToggleSwitchStrokeOff" ResourceKey="SystemControlForegroundBaseMediumBrush" />
<StaticResource x:Key="ToggleSwitchStrokeOffPointerOver" ResourceKey="SystemControlHighlightBaseMediumHighBrush" />
<StaticResource x:Key="ToggleSwitchStrokeOffPressed" ResourceKey="SystemControlForegroundBaseHighBrush" />
<StaticResource x:Key="ToggleSwitchStrokeOffDisabled" ResourceKey="SystemControlDisabledBaseMediumLowBrush" />
<StaticResource x:Key="ToggleSwitchFillOn" ResourceKey="SystemControlHighlightAccentBrush" />
<StaticResource x:Key="ToggleSwitchFillOnPointerOver" ResourceKey="SystemAccentColorLight1" />
<StaticResource x:Key="ToggleSwitchFillOnPressed" ResourceKey="SystemAccentColorDark1" />
<StaticResource x:Key="ToggleSwitchFillOnDisabled" ResourceKey="SystemControlDisabledBaseLowBrush" />
<StaticResource x:Key="ToggleSwitchStrokeOn" ResourceKey="SystemControlHighlightBaseHighBrush" />
<StaticResource x:Key="ToggleSwitchStrokeOnPointerOver" ResourceKey="SystemAccentColorLight1" />
<StaticResource x:Key="ToggleSwitchStrokeOnPressed" ResourceKey="SystemAccentColorDark1" />
<StaticResource x:Key="ToggleSwitchStrokeOnDisabled" ResourceKey="SystemControlDisabledBaseMediumLowBrush" />
<StaticResource x:Key="ToggleSwitchKnobFillOff" ResourceKey="SystemControlHighlightBaseHighBrush" />
<StaticResource x:Key="ToggleSwitchKnobFillOffPointerOver" ResourceKey="SystemControlHighlightBaseHighBrush" />
<StaticResource x:Key="ToggleSwitchKnobFillOffPressed" ResourceKey="SystemControlHighlightBaseHighBrush" />
<StaticResource x:Key="ToggleSwitchKnobFillOffDisabled" ResourceKey="SystemControlDisabledBaseMediumLowBrush" />
<StaticResource x:Key="ToggleSwitchKnobFillOn" ResourceKey="SystemControlHighlightAltChromeWhiteBrush" />
<StaticResource x:Key="ToggleSwitchKnobFillOnPointerOver" ResourceKey="SystemControlHighlightChromeWhiteBrush" />
<StaticResource x:Key="ToggleSwitchKnobFillOnPressed" ResourceKey="SystemControlHighlightAltChromeWhiteBrush" />
<StaticResource x:Key="ToggleSwitchKnobFillOnDisabled" ResourceKey="SystemControlPageBackgroundBaseLowBrush" />
<SolidColorBrush x:Key="ToggleSwitchCurtainBackgroundThemeBrush" Color="#FF5729C1" />
<SolidColorBrush x:Key="ToggleSwitchCurtainDisabledBackgroundThemeBrush" Color="Transparent" />
<SolidColorBrush x:Key="ToggleSwitchCurtainPointerOverBackgroundThemeBrush" Color="#FF6E46CA" />
<SolidColorBrush x:Key="ToggleSwitchCurtainPressedBackgroundThemeBrush" Color="#FF7E4FEC" />
<SolidColorBrush x:Key="ToggleSwitchDisabledForegroundThemeBrush" Color="#66FFFFFF" />
<SolidColorBrush x:Key="ToggleSwitchForegroundThemeBrush" Color="#FFFFFFFF" />
<SolidColorBrush x:Key="ToggleSwitchHeaderDisabledForegroundThemeBrush" Color="#66FFFFFF" />
<SolidColorBrush x:Key="ToggleSwitchHeaderForegroundThemeBrush" Color="#FFFFFFFF" />
<SolidColorBrush x:Key="ToggleSwitchOuterBorderBorderThemeBrush" Color="#59FFFFFF" />
<SolidColorBrush x:Key="ToggleSwitchOuterBorderDisabledBorderThemeBrush" Color="#33FFFFFF" />
<SolidColorBrush x:Key="ToggleSwitchThumbBackgroundThemeBrush" Color="#FFFFFFFF" />
<SolidColorBrush x:Key="ToggleSwitchThumbBorderThemeBrush" Color="#FFFFFFFF" />
<SolidColorBrush x:Key="ToggleSwitchThumbDisabledBackgroundThemeBrush" Color="#FF7E7E7E" />
<SolidColorBrush x:Key="ToggleSwitchThumbDisabledBorderThemeBrush" Color="#FF7E7E7E" />
<SolidColorBrush x:Key="ToggleSwitchThumbPointerOverBackgroundThemeBrush" Color="#FFFFFFFF" />
<SolidColorBrush x:Key="ToggleSwitchThumbPointerOverBorderThemeBrush" Color="#FFFFFFFF" />
<SolidColorBrush x:Key="ToggleSwitchThumbPressedBackgroundThemeBrush" Color="#FFFFFFFF" />
<SolidColorBrush x:Key="ToggleSwitchThumbPressedForegroundThemeBrush" Color="#FFFFFFFF" />
<SolidColorBrush x:Key="ToggleSwitchTrackBackgroundThemeBrush" Color="#42FFFFFF" />
<SolidColorBrush x:Key="ToggleSwitchTrackBorderThemeBrush" Color="Transparent" />
<SolidColorBrush x:Key="ToggleSwitchTrackDisabledBackgroundThemeBrush" Color="#1FFFFFFF" />
<SolidColorBrush x:Key="ToggleSwitchTrackPointerOverBackgroundThemeBrush" Color="#4AFFFFFF" />
<SolidColorBrush x:Key="ToggleSwitchTrackPressedBackgroundThemeBrush" Color="#59FFFFFF" />
<!-- Resources for ToolTip.xaml -->
<x:Double x:Key="ToolTipContentThemeFontSize">12</x:Double>
<Thickness x:Key="ToolTipBorderThemeThickness">1</Thickness>

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

@ -1,5 +1,5 @@
<Style xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
<Style xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=netstandard">
<Style.Resources>
<!-- Resources for Button.xaml -->
@ -181,8 +181,8 @@
<StaticResource x:Key="ComboBoxDropDownGlyphForeground" ResourceKey="SystemControlForegroundBaseMediumHighBrush" />
<StaticResource x:Key="ComboBoxDropDownGlyphForegroundDisabled" ResourceKey="SystemControlDisabledBaseMediumLowBrush" />
<StaticResource x:Key="ComboBoxDropDownGlyphForegroundFocused" ResourceKey="SystemControlHighlightAltBaseMediumHighBrush" />
<StaticResource x:Key="ComboBoxDropDownGlyphForegroundFocusedPressed" ResourceKey="SystemControlHighlightAltBaseMediumHighBrush" />
<StaticResource x:Key="ComboBoxDropDownForeground" ResourceKey="SystemControlForegroundBaseHighBrush" />
<StaticResource x:Key="ComboBoxDropDownGlyphForegroundFocusedPressed" ResourceKey="SystemControlHighlightAltBaseMediumHighBrush" />
<StaticResource x:Key="ComboBoxDropDownForeground" ResourceKey="SystemControlForegroundBaseHighBrush" />
<StaticResource x:Key="ComboBoxDropDownBackground" ResourceKey="SystemControlTransientBackgroundBrush" />
<StaticResource x:Key="ComboBoxDropDownBorderBrush" ResourceKey="SystemControlTransientBorderBrush" />
@ -230,7 +230,7 @@
<StaticResource x:Key="ComboBoxFocusedDropDownBackgroundPointerOver" ResourceKey="SystemControlBackgroundBaseLowBrush" />
<StaticResource x:Key="ComboBoxFocusedDropDownBackgroundPointerPressed" ResourceKey="SystemControlBackgroundBaseMediumLowBrush" />
<StaticResource x:Key="ComboBoxEditableDropDownGlyphForeground" ResourceKey="SystemControlForegroundBaseMediumHighBrush" />
<!-- Resources for ListBox.xaml -->
<Thickness x:Key="ListBoxBorderThemeThickness">0</Thickness>
<SolidColorBrush x:Key="ListBoxBackgroundThemeBrush" Color="#CCFFFFFF" />
@ -257,6 +257,91 @@
<SolidColorBrush x:Key="ProgressBarForegroundThemeBrush" Color="#FF4617B4" />
<SolidColorBrush x:Key="ProgressBarIndeterminateForegroundThemeBrush" Color="#FF4617B4" />
<!-- Resources for MenuFlyout.xaml (Menu, ContextMenu, etc) -->
<x:Double x:Key="MenuFlyoutSeparatorThemeHeight">1</x:Double>
<x:Double x:Key="MenuFlyoutThemeMinHeight">32</x:Double>
<Thickness x:Key="MenuFlyoutPresenterThemePadding">0,0</Thickness>
<!--<Thickness x:Key="MenuFlyoutItemCheckGlyphMargin">12,11,0,13</Thickness>-->
<Thickness x:Key="MenuFlyoutItemChevronMargin">12,0,0,0</Thickness>
<!--<Thickness x:Key="MenuFlyoutItemPlaceholderThemeThickness">28,0,0,0</Thickness>-->
<Thickness x:Key="MenuFlyoutSeparatorThemePadding">12,4,12,4</Thickness>
<StaticResource x:Key="MenuFlyoutItemBackground" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="MenuFlyoutItemBackgroundPointerOver" ResourceKey="SystemControlHighlightListLowBrush" />
<StaticResource x:Key="MenuFlyoutItemBackgroundPressed" ResourceKey="SystemControlHighlightListMediumBrush" />
<StaticResource x:Key="MenuFlyoutItemBackgroundDisabled" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="MenuFlyoutItemForeground" ResourceKey="SystemControlForegroundBaseHighBrush" />
<StaticResource x:Key="MenuFlyoutItemForegroundPointerOver" ResourceKey="SystemControlHighlightAltBaseHighBrush" />
<StaticResource x:Key="MenuFlyoutItemForegroundPressed" ResourceKey="SystemControlHighlightAltBaseHighBrush" />
<StaticResource x:Key="MenuFlyoutItemForegroundDisabled" ResourceKey="SystemControlDisabledBaseMediumLowBrush" />
<!--<StaticResource x:Key="MenuFlyoutSubItemBackground" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="MenuFlyoutSubItemBackgroundPointerOver" ResourceKey="SystemControlHighlightListLowBrush" />
<StaticResource x:Key="MenuFlyoutSubItemBackgroundPressed" ResourceKey="SystemControlHighlightListAccentHighBrush" />
<StaticResource x:Key="MenuFlyoutSubItemBackgroundSubMenuOpened" ResourceKey="SystemControlHighlightListLowBrush" />
<StaticResource x:Key="MenuFlyoutSubItemBackgroundDisabled" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="MenuFlyoutSubItemForeground" ResourceKey="SystemControlForegroundBaseHighBrush" />
<StaticResource x:Key="MenuFlyoutSubItemForegroundPointerOver" ResourceKey="SystemControlHighlightAltBaseHighBrush" />
<StaticResource x:Key="MenuFlyoutSubItemForegroundPressed" ResourceKey="SystemControlHighlightAltBaseHighBrush" />
<StaticResource x:Key="MenuFlyoutSubItemForegroundSubMenuOpened" ResourceKey="SystemControlHighlightAltBaseHighBrush" />
<StaticResource x:Key="MenuFlyoutSubItemForegroundDisabled" ResourceKey="SystemControlDisabledBaseMediumLowBrush" />-->
<StaticResource x:Key="MenuFlyoutSubItemChevron" ResourceKey="SystemControlForegroundBaseMediumHighBrush" />
<StaticResource x:Key="MenuFlyoutSubItemChevronPointerOver" ResourceKey="SystemControlHighlightAltBaseHighBrush" />
<StaticResource x:Key="MenuFlyoutSubItemChevronPressed" ResourceKey="SystemControlHighlightAltBaseHighBrush" />
<StaticResource x:Key="MenuFlyoutSubItemChevronSubMenuOpened" ResourceKey="SystemControlHighlightAltBaseHighBrush" />
<StaticResource x:Key="MenuFlyoutSubItemChevronDisabled" ResourceKey="SystemControlDisabledBaseMediumLowBrush" />
<StaticResource x:Key="MenuFlyoutLightDismissOverlayBackground" ResourceKey="SystemControlPageBackgroundMediumAltMediumBrush" />
<!--<SolidColorBrush x:Key="MenuFlyoutItemFocusedBackgroundThemeBrush" Color="#FFE5E5E5" />
<SolidColorBrush x:Key="MenuFlyoutItemFocusedForegroundThemeBrush" Color="#FF000000" />
<SolidColorBrush x:Key="MenuFlyoutItemDisabledForegroundThemeBrush" Color="#66000000" />
<SolidColorBrush x:Key="MenuFlyoutItemPointerOverBackgroundThemeBrush" Color="#FFE5E5E5" />
<SolidColorBrush x:Key="MenuFlyoutItemPointerOverForegroundThemeBrush" Color="#FF000000" />
<SolidColorBrush x:Key="MenuFlyoutItemPressedBackgroundThemeBrush" Color="#FF000000" />
<SolidColorBrush x:Key="MenuFlyoutItemPressedForegroundThemeBrush" Color="#FFFFFFFF" />
<SolidColorBrush x:Key="MenuFlyoutSeparatorThemeBrush" Color="#FF7A7A7A" />-->
<!--<Thickness x:Key="MenuFlyoutItemDoublePlaceholderThemeThickness">56,0,0,0</Thickness>-->
<StaticResource x:Key="MenuFlyoutItemKeyboardAcceleratorTextForeground" ResourceKey="SystemControlForegroundBaseMediumBrush" />
<StaticResource x:Key="MenuFlyoutItemKeyboardAcceleratorTextForegroundPointerOver" ResourceKey="SystemControlHighlightAltBaseMediumBrush" />
<StaticResource x:Key="MenuFlyoutItemKeyboardAcceleratorTextForegroundPressed" ResourceKey="SystemControlHighlightAltBaseMediumBrush" />
<StaticResource x:Key="MenuFlyoutItemKeyboardAcceleratorTextForegroundDisabled" ResourceKey="SystemControlDisabledBaseMediumLowBrush" />
<Thickness x:Key="MenuFlyoutItemThemePadding">11,9,11,10</Thickness>
<Thickness x:Key="MenuFlyoutItemThemePaddingNarrow">11,4,11,7</Thickness>
<StaticResource x:Key="MenuFlyoutPresenterBackground" ResourceKey="SystemControlTransientBackgroundBrush" />
<StaticResource x:Key="MenuFlyoutPresenterBorderBrush" ResourceKey="SystemControlTransientBorderBrush" />
<Thickness x:Key="MenuFlyoutPresenterBorderThemeThickness">1</Thickness>
<!-- Resources for MenuFlyoutItem -->
<!--<StaticResource x:Key="MenuFlyoutItemRevealBackground" ResourceKey="SystemControlTransparentRevealBackgroundBrush" />
<StaticResource x:Key="MenuFlyoutItemRevealBackgroundPointerOver" ResourceKey="SystemControlHighlightListLowRevealBackgroundBrush" />
<StaticResource x:Key="MenuFlyoutItemRevealBackgroundPressed" ResourceKey="SystemControlHighlightListMediumRevealBackgroundBrush" />
<StaticResource x:Key="MenuFlyoutItemRevealBackgroundDisabled" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="MenuFlyoutItemRevealBorderBrush" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="MenuFlyoutItemRevealBorderBrushPressed" ResourceKey="SystemControlTransparentRevealBorderBrush" />
<StaticResource x:Key="MenuFlyoutItemRevealBorderBrushPointerOver" ResourceKey="SystemControlTransparentRevealBorderBrush" />
<StaticResource x:Key="MenuFlyoutItemRevealBorderBrushDisabled" ResourceKey="SystemControlTransparentBrush" />
-->
<!-- Resources for ToggleMenuFlyoutItem -->
<!--
<StaticResource x:Key="ToggleMenuFlyoutItemRevealBackground" ResourceKey="SystemControlTransparentRevealBackgroundBrush" />
<StaticResource x:Key="ToggleMenuFlyoutItemRevealBackgroundPointerOver" ResourceKey="SystemControlHighlightListLowRevealBackgroundBrush" />
<StaticResource x:Key="ToggleMenuFlyoutItemRevealBackgroundPressed" ResourceKey="SystemControlHighlightListMediumRevealBackgroundBrush" />
<StaticResource x:Key="ToggleMenuFlyoutItemRevealBackgroundDisabled" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="ToggleMenuFlyoutItemRevealBorderBrush" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="ToggleMenuFlyoutItemRevealBorderBrushPressed" ResourceKey="SystemControlTransparentRevealBorderBrush" />
<StaticResource x:Key="ToggleMenuFlyoutItemRevealBorderBrushPointerOver" ResourceKey="SystemControlTransparentRevealBorderBrush" />
<StaticResource x:Key="ToggleMenuFlyoutItemRevealBorderBrushDisabled" ResourceKey="SystemControlTransparentBrush" />
-->
<!-- Resources for MenuFlyoutSubItem -->
<!--
<StaticResource x:Key="MenuFlyoutSubItemRevealBackground" ResourceKey="SystemControlTransparentRevealBackgroundBrush" />
<StaticResource x:Key="MenuFlyoutSubItemRevealBackgroundPointerOver" ResourceKey="SystemControlHighlightListLowRevealBackgroundBrush" />
<StaticResource x:Key="MenuFlyoutSubItemRevealBackgroundPressed" ResourceKey="SystemControlHighlightAccentRevealBackgroundBrush" />
<StaticResource x:Key="MenuFlyoutSubItemRevealBackgroundSubMenuOpened" ResourceKey="SystemControlHighlightListLowRevealBackgroundBrush" />
<StaticResource x:Key="MenuFlyoutSubItemRevealBackgroundDisabled" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="MenuFlyoutSubItemRevealBorderBrush" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="MenuFlyoutSubItemRevealBorderBrushPressed" ResourceKey="SystemControlTransparentRevealBorderBrush" />
<StaticResource x:Key="MenuFlyoutSubItemRevealBorderBrushPointerOver" ResourceKey="SystemControlTransparentRevealBorderBrush" />
<StaticResource x:Key="MenuFlyoutSubItemRevealBorderBrushSubMenuOpened" ResourceKey="SystemControlTransparentRevealBorderBrush" />
<StaticResource x:Key="MenuFlyoutSubItemRevealBorderBrushDisabled" ResourceKey="SystemControlTransparentBrush" />
<Thickness x:Key="LanguageSwitcherMenuFlyoutItemPlaceholderThemeThickness">44,0,0,0</Thickness>-->
<!-- Resources for TextBox.xaml -->
<SolidColorBrush x:Key="TextBoxForegroundHeaderThemeBrush" Color="#FF000000" />
<SolidColorBrush x:Key="TextBoxPlaceholderTextThemeBrush" Color="#AB000000" />
@ -458,7 +543,7 @@
<SolidColorBrush x:Key="RadioButtonPressedBorderThemeBrush" Color="#FF000000" />
<SolidColorBrush x:Key="RadioButtonPressedForegroundThemeBrush" Color="#FFFFFFFF" />
<SolidColorBrush x:Key="RadioButtonContentPointerOverForegroundThemeBrush" Color="{DynamicResource SystemColorHighlightTextColor}" />
<!-- Resources for Slider.xaml -->
<x:Double x:Key="SliderOutsideTickBarThemeHeight">4</x:Double>
<x:Double x:Key="SliderTrackThemeHeight">2</x:Double>
@ -509,6 +594,65 @@
<SolidColorBrush x:Key="SliderTrackPressedBackgroundThemeBrush" Color="#33000000" />
<SolidColorBrush x:Key="SliderHeaderForegroundThemeBrush" Color="#FF000000" />
<!--Recources ToggleSwitch-->
<Thickness x:Key="ToggleSwitchOnStrokeThickness">0</Thickness>
<Thickness x:Key="ToggleSwitchOuterBorderStrokeThickness">1</Thickness>
<StaticResource x:Key="ToggleSwitchContentForeground" ResourceKey="SystemControlForegroundBaseHighBrush" />
<StaticResource x:Key="ToggleSwitchContentForegroundDisabled" ResourceKey="SystemControlDisabledBaseMediumLowBrush" />
<StaticResource x:Key="ToggleSwitchHeaderForeground" ResourceKey="SystemControlForegroundBaseHighBrush" />
<StaticResource x:Key="ToggleSwitchHeaderForegroundDisabled" ResourceKey="SystemControlDisabledBaseMediumLowBrush" />
<StaticResource x:Key="ToggleSwitchContainerBackground" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="ToggleSwitchContainerBackgroundPointerOver" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="ToggleSwitchContainerBackgroundPressed" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="ToggleSwitchContainerBackgroundDisabled" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="ToggleSwitchFillOff" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="ToggleSwitchFillOffPointerOver" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="ToggleSwitchFillOffPressed" ResourceKey="SystemControlHighlightBaseMediumLowBrush" />
<StaticResource x:Key="ToggleSwitchFillOffDisabled" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="ToggleSwitchStrokeOff" ResourceKey="SystemControlForegroundBaseMediumBrush" />
<StaticResource x:Key="ToggleSwitchStrokeOffPointerOver" ResourceKey="SystemControlHighlightBaseMediumHighBrush" />
<StaticResource x:Key="ToggleSwitchStrokeOffPressed" ResourceKey="SystemControlForegroundBaseHighBrush" />
<StaticResource x:Key="ToggleSwitchStrokeOffDisabled" ResourceKey="SystemControlDisabledBaseMediumLowBrush" />
<StaticResource x:Key="ToggleSwitchFillOn" ResourceKey="SystemControlHighlightAccentBrush" />
<StaticResource x:Key="ToggleSwitchFillOnPointerOver" ResourceKey="SystemAccentColorLight1" />
<StaticResource x:Key="ToggleSwitchFillOnPressed" ResourceKey="SystemAccentColorDark1" />
<StaticResource x:Key="ToggleSwitchFillOnDisabled" ResourceKey="SystemControlDisabledBaseLowBrush" />
<StaticResource x:Key="ToggleSwitchStrokeOn" ResourceKey="SystemControlHighlightBaseHighBrush" />
<StaticResource x:Key="ToggleSwitchStrokeOnPointerOver" ResourceKey="SystemControlHighlightListAccentHighBrush" />
<StaticResource x:Key="ToggleSwitchStrokeOnPressed" ResourceKey="SystemControlHighlightBaseMediumBrush" />
<StaticResource x:Key="ToggleSwitchStrokeOnDisabled" ResourceKey="SystemControlDisabledBaseMediumLowBrush" />
<StaticResource x:Key="ToggleSwitchKnobFillOff" ResourceKey="SystemControlForegroundBaseHighBrush" />
<StaticResource x:Key="ToggleSwitchKnobFillOffPointerOver" ResourceKey="SystemControlHighlightBaseHighBrush" />
<StaticResource x:Key="ToggleSwitchKnobFillOffPressed" ResourceKey="SystemControlHighlightBaseHighBrush" />
<StaticResource x:Key="ToggleSwitchKnobFillOffDisabled" ResourceKey="SystemControlDisabledBaseMediumLowBrush" />
<StaticResource x:Key="ToggleSwitchKnobFillOn" ResourceKey="SystemControlHighlightAltChromeWhiteBrush" />
<StaticResource x:Key="ToggleSwitchKnobFillOnPointerOver" ResourceKey="SystemControlHighlightChromeWhiteBrush" />
<StaticResource x:Key="ToggleSwitchKnobFillOnPressed" ResourceKey="SystemControlHighlightAltChromeWhiteBrush" />
<StaticResource x:Key="ToggleSwitchKnobFillOnDisabled" ResourceKey="SystemControlPageBackgroundBaseLowBrush" />
<SolidColorBrush x:Key="ToggleSwitchCurtainBackgroundThemeBrush" Color="#FF4617B4" />
<SolidColorBrush x:Key="ToggleSwitchCurtainDisabledBackgroundThemeBrush" Color="Transparent" />
<SolidColorBrush x:Key="ToggleSwitchCurtainPointerOverBackgroundThemeBrush" Color="#FF5F37BE" />
<SolidColorBrush x:Key="ToggleSwitchCurtainPressedBackgroundThemeBrush" Color="#FF7241E4" />
<SolidColorBrush x:Key="ToggleSwitchDisabledForegroundThemeBrush" Color="#66000000" />
<SolidColorBrush x:Key="ToggleSwitchForegroundThemeBrush" Color="#FF000000" />
<SolidColorBrush x:Key="ToggleSwitchHeaderDisabledForegroundThemeBrush" Color="#66000000" />
<SolidColorBrush x:Key="ToggleSwitchHeaderForegroundThemeBrush" Color="#FF000000" />
<SolidColorBrush x:Key="ToggleSwitchOuterBorderBorderThemeBrush" Color="#59000000" />
<SolidColorBrush x:Key="ToggleSwitchOuterBorderDisabledBorderThemeBrush" Color="#33000000" />
<SolidColorBrush x:Key="ToggleSwitchThumbBackgroundThemeBrush" Color="#FF000000" />
<SolidColorBrush x:Key="ToggleSwitchThumbBorderThemeBrush" Color="#FF000000" />
<SolidColorBrush x:Key="ToggleSwitchThumbDisabledBackgroundThemeBrush" Color="#FF929292" />
<SolidColorBrush x:Key="ToggleSwitchThumbDisabledBorderThemeBrush" Color="#FF929292" />
<SolidColorBrush x:Key="ToggleSwitchThumbPointerOverBackgroundThemeBrush" Color="#FF000000" />
<SolidColorBrush x:Key="ToggleSwitchThumbPointerOverBorderThemeBrush" Color="#FF000000" />
<SolidColorBrush x:Key="ToggleSwitchThumbPressedBackgroundThemeBrush" Color="#FF000000" />
<SolidColorBrush x:Key="ToggleSwitchThumbPressedForegroundThemeBrush" Color="#FF000000" />
<SolidColorBrush x:Key="ToggleSwitchTrackBackgroundThemeBrush" Color="#59000000" />
<SolidColorBrush x:Key="ToggleSwitchTrackBorderThemeBrush" Color="Transparent" />
<SolidColorBrush x:Key="ToggleSwitchTrackDisabledBackgroundThemeBrush" Color="#1F000000" />
<SolidColorBrush x:Key="ToggleSwitchTrackPointerOverBackgroundThemeBrush" Color="#4A000000" />
<SolidColorBrush x:Key="ToggleSwitchTrackPressedBackgroundThemeBrush" Color="#42000000" />
<!-- Resources for ToolTip.xaml -->
<x:Double x:Key="ToolTipContentThemeFontSize">12</x:Double>
<Thickness x:Key="ToolTipBorderThemeThickness">1</Thickness>

8
src/Avalonia.Themes.Fluent/Button.xaml

@ -54,16 +54,16 @@
<Setter Property="TextBlock.Foreground" Value="{DynamicResource ButtonForegroundPressed}" />
</Style>
<Style Selector="Button:disabled">
<Style Selector="Button:disabled /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource ButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" />
<Setter Property="TextBlock.Foreground" Value="{DynamicResource ButtonForegroundDisabled}" />
</Style>
<Style Selector="Button.accent">
<Setter Property="Foreground" Value="{DynamicResource AccentButtonForeground}" />
<Style Selector="Button.accent /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AccentButtonBackground}" />
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrush}" />
<Setter Property="TextBlock.Foreground" Value="{DynamicResource AccentButtonForeground}" />
</Style>
<Style Selector="Button.accent:pointerover /template/ ContentPresenter">
@ -78,7 +78,7 @@
<Setter Property="TextBlock.Foreground" Value="{DynamicResource AccentButtonForegroundPressed}" />
</Style>
<Style Selector="Button.accent:disabled">
<Style Selector="Button.accent:disabled /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource AccentButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource AccentButtonBorderBrushDisabled}" />
<Setter Property="TextBlock.Foreground" Value="{DynamicResource AccentButtonForegroundDisabled}" />

200
src/Avalonia.Themes.Fluent/ButtonSpinner.xaml

@ -1,104 +1,140 @@
<Styles xmlns="https://github.com/avaloniaui">
<Style Selector="ButtonSpinner">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
<Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/>
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=netstandard"
xmlns:converters="clr-namespace:Avalonia.Controls.Converters;assembly=Avalonia.Controls">
<Design.PreviewWith>
<Border Padding="20"
Background="Black">
<StackPanel Spacing="20">
<ButtonSpinner ButtonSpinnerLocation="Right"
Content="Right disabled inline spinner"
AllowSpin="False" />
<ButtonSpinner ButtonSpinnerLocation="Left"
Content="Left spinner" />
<ButtonSpinner ShowButtonSpinner="False"
Content="Hide spinner" />
</StackPanel>
</Border>
</Design.PreviewWith>
<Styles.Resources>
<converters:MarginMultiplierConverter x:Key="ButtonSpinner_OnlyLeftThinknessConverter"
Indent="1"
Left="True" />
<converters:MarginMultiplierConverter x:Key="ButtonSpinner_OnlyRightThinknessConverter"
Indent="1"
Right="True" />
<StreamGeometry x:Key="ButtonSpinnerIncreaseButtonIcon">M0,9 L10,0 20,9 19,10 10,2 1,10 z</StreamGeometry>
<StreamGeometry x:Key="ButtonSpinnerDecreaseButtonIcon">M0,1 L10,10 20,1 19,0 10,8 1,0 z</StreamGeometry>
</Styles.Resources>
<!-- RepeatButton.ButtonSpinnerRepeatButton -->
<Style Selector="RepeatButton.ButtonSpinnerRepeatButton">
<Setter Property="MinWidth" Value="34" />
<Setter Property="VerticalAlignment" Value="Stretch" />
</Style>
<Style Selector="ButtonSpinner /template/ RepeatButton">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Style Selector="RepeatButton.ButtonSpinnerRepeatButton /template/ ContentPresenter">
<Setter Property="CornerRadius" Value="0" />
<Setter Property="BorderBrush" Value="{TemplateBinding BorderBrush}" />
</Style>
<Style Selector="ButtonSpinner /template/ RepeatButton:pointerover">
<Setter Property="Background" Value="{DynamicResource ThemeControlMidBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
<Style Selector="RepeatButton.ButtonSpinnerRepeatButton:disabled">
<Setter Property="BorderBrush" Value="{TemplateBinding BorderBrush}" />
</Style>
<Style Selector="ButtonSpinner /template/ RepeatButton#PART_IncreaseButton">
<Setter Property="Content">
<Template>
<Path Fill="{DynamicResource ThemeForegroundBrush}"
Width="8"
Height="4"
Stretch="Uniform"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="M0,5 L4.5,.5 9,5 6,5 4.5,3.5 3,5 z"/>
</Template>
</Setter>
<Style Selector="RepeatButton.ButtonSpinnerRepeatButton:disabled > Path">
<Setter Property="Fill" Value="{DynamicResource RepeatButtonForegroundDisabled}" />
</Style>
<Style Selector="ButtonSpinner /template/ RepeatButton#PART_DecreaseButton">
<Setter Property="Content">
<Template>
<Path Fill="{DynamicResource ThemeForegroundBrush}"
Width="8"
Height="4"
Stretch="Uniform"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="M0,0 L3,0 4.5,1.5 6,0 9,0 4.5,4.5 z"/>
</Template>
</Setter>
</Style>
<Style Selector="ButtonSpinner:right">
<!-- ButtonSpinner -->
<Style Selector="ButtonSpinner">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="{DynamicResource TextControlForeground}" />
<Setter Property="Padding" Value="10, 0" />
<Setter Property="Background" Value="{DynamicResource TextControlBackground}" />
<Setter Property="BorderBrush" Value="{DynamicResource TextControlBorderBrush}" />
<Setter Property="BorderThickness" Value="{DynamicResource TextControlBorderThemeThickness}" />
<Setter Property="MinHeight" Value="{DynamicResource TextControlThemeMinHeight}" />
<Setter Property="MinWidth" Value="{DynamicResource TextControlThemeMinWidth}" />
<Setter Property="FontFamily" Value="{DynamicResource ContentControlThemeFontFamily}" />
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="Template">
<ControlTemplate>
<Border Name="border"
Background="{TemplateBinding Background}"
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}">
<Grid ColumnDefinitions="*,Auto">
<ContentPresenter Name="PART_ContentPresenter" Grid.Column="0"
CornerRadius="{DynamicResource ControlCornerRadius}">
<Grid ColumnDefinitions="Auto,*,Auto">
<ContentPresenter Name="PART_ContentPresenter"
Grid.Column="1"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
Padding="{TemplateBinding Padding}"/>
<Grid Grid.Column="1" RowDefinitions="*,*" IsVisible="{TemplateBinding ShowButtonSpinner}">
<RepeatButton Grid.Row="0" Name="PART_IncreaseButton"/>
<RepeatButton Grid.Row="1" Name="PART_DecreaseButton"/>
</Grid>
Padding="{TemplateBinding Padding}" />
<StackPanel Name="PART_SpinnerPanel"
Orientation="Horizontal"
IsVisible="{TemplateBinding ShowButtonSpinner}">
<RepeatButton Name="PART_IncreaseButton"
Classes="ButtonSpinnerRepeatButton"
VerticalContentAlignment="Center"
Foreground="{TemplateBinding Foreground}"
BorderBrush="{TemplateBinding BorderBrush}"
Background="{TemplateBinding Background}"
FontSize="{TemplateBinding FontSize}">
<Path Fill="{TemplateBinding Foreground}"
Width="16"
Height="8"
Stretch="Uniform"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="{StaticResource ButtonSpinnerIncreaseButtonIcon}" />
</RepeatButton>
<RepeatButton Name="PART_DecreaseButton"
Classes="ButtonSpinnerRepeatButton"
Foreground="{TemplateBinding Foreground}"
BorderBrush="{TemplateBinding BorderBrush}"
Background="{TemplateBinding Background}"
VerticalContentAlignment="Center"
FontSize="{TemplateBinding FontSize}">
<Path Fill="{TemplateBinding Foreground}"
Width="16"
Height="8"
Stretch="Uniform"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Data="{StaticResource ButtonSpinnerDecreaseButtonIcon}" />
</RepeatButton>
</StackPanel>
</Grid>
</Border>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="ButtonSpinner:left">
<Setter Property="Template">
<ControlTemplate>
<Border Name="border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}">
<Grid ColumnDefinitions="Auto,*">
<Grid Grid.Column="0" RowDefinitions="*,*" IsVisible="{TemplateBinding ShowButtonSpinner}">
<RepeatButton Grid.Row="0" Name="PART_IncreaseButton"/>
<RepeatButton Grid.Row="1" Name="PART_DecreaseButton"/>
</Grid>
<ContentPresenter Name="PART_ContentPresenter" Grid.Column="1"
ContentTemplate="{TemplateBinding ContentTemplate}"
Content="{TemplateBinding Content}"
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"
Padding="{TemplateBinding Padding}"/>
</Grid>
</Border>
</ControlTemplate>
</Setter>
<!-- ButtonSpinnerLocation=Right -->
<Style Selector="ButtonSpinner:right /template/ StackPanel#PART_SpinnerPanel">
<Setter Property="Grid.Column" Value="2" />
</Style>
<Style Selector="ButtonSpinner:right /template/ RepeatButton.ButtonSpinnerRepeatButton">
<Setter Property="BorderThickness" Value="{Binding $parent[ButtonSpinner].BorderThickness, Converter={StaticResource ButtonSpinner_OnlyLeftThinknessConverter}}" />
</Style>
<Style Selector="ButtonSpinner:pointerover /template/ Border#border">
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderHighBrush}"/>
<!-- ButtonSpinnerLocation=Left -->
<Style Selector="ButtonSpinner:left /template/ StackPanel#PART_SpinnerPanel">
<Setter Property="Grid.Column" Value="0" />
</Style>
<Style Selector="ButtonSpinner:focus /template/ Border#border">
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderHighBrush}"/>
<Style Selector="ButtonSpinner:left /template/ RepeatButton.ButtonSpinnerRepeatButton">
<Setter Property="BorderThickness" Value="{Binding $parent[ButtonSpinner].BorderThickness, Converter={StaticResource ButtonSpinner_OnlyRightThinknessConverter}}" />
</Style>
<Style Selector="ButtonSpinner:error /template/ Border#border">
<Setter Property="BorderBrush" Value="{DynamicResource ErrorBrush}"/>
<!-- Error state -->
<Style Selector="ButtonSpinner:error">
<Setter Property="BorderBrush" Value="{DynamicResource ErrorBrush}" />
</Style>
</Styles>

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

@ -0,0 +1,6 @@
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style Selector="TextBlock.CaptionTextBlockStyle">
<Setter Property="FontSize" Value="12" />
<Setter Property="FontWeight" Value="Normal" />
</Style>
</Styles>

69
src/Avalonia.Themes.Fluent/ContextMenu.xaml

@ -1,22 +1,61 @@
<Style xmlns="https://github.com/avaloniaui" Selector="ContextMenu">
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="4,2"/>
<Setter Property="TextBlock.FontSize" Value="{DynamicResource FontSizeNormal}" />
<Setter Property="TextBlock.FontWeight" Value="Normal" />
<Setter Property="Template">
<Style xmlns="https://github.com/avaloniaui"
Selector="ContextMenu">
<Design.PreviewWith>
<Border Background="{DynamicResource ThemeAccentBrush}"
Margin="16"
Padding="48"
Width="400"
Height="200">
<Border.ContextMenu>
<ContextMenu>
<MenuItem Header="Standard _Menu Item" />
<Separator />
<MenuItem Header="Menu with _Submenu">
<MenuItem Header="Submenu _1" />
<MenuItem Header="Submenu _2" />
</MenuItem>
<MenuItem Header="Menu Item with _Icon" />
<MenuItem Header="Menu Item with _Checkbox">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsHitTestVisible="False"
IsChecked="True" />
</MenuItem.Icon>
</MenuItem>
</ContextMenu>
</Border.ContextMenu>
<TextBlock Text="Defined in XAML" />
</Border>
</Design.PreviewWith>
<Setter Property="Background" Value="{DynamicResource MenuFlyoutPresenterBackground}" />
<Setter Property="BorderBrush" Value="{DynamicResource MenuFlyoutPresenterBorderBrush}" />
<Setter Property="BorderThickness" Value="{DynamicResource MenuFlyoutPresenterBorderThemeThickness}" />
<Setter Property="MaxWidth" Value="{DynamicResource FlyoutThemeMaxWidth}" />
<Setter Property="MinHeight" Value="{DynamicResource MenuFlyoutThemeMinHeight}" />
<Setter Property="Padding" Value="{DynamicResource MenuFlyoutPresenterThemePadding}" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="TextBlock.FontSize" Value="{DynamicResource FontSizeNormal}" />
<Setter Property="TextBlock.FontWeight" Value="Normal" />
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Padding="{TemplateBinding Padding}">
<ScrollViewer>
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
KeyboardNavigation.TabNavigation="Continue"/>
</ScrollViewer>
Padding="{TemplateBinding Padding}"
MaxWidth="{TemplateBinding MaxWidth}"
MinHeight="{TemplateBinding MinHeight}"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
CornerRadius="{DynamicResource OverlayCornerRadius}">
<ScrollViewer>
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
Margin="{DynamicResource MenuFlyoutScrollerMargin}"
KeyboardNavigation.TabNavigation="Continue"
Grid.IsSharedSizeScope="True" />
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter>

1
src/Avalonia.Themes.Fluent/FluentTheme.xaml

@ -52,4 +52,5 @@
<StyleInclude Source="resm:Avalonia.Themes.Fluent.WindowNotificationManager.xaml?assembly=Avalonia.Themes.Fluent"/>
<StyleInclude Source="resm:Avalonia.Themes.Fluent.NotificationCard.xaml?assembly=Avalonia.Themes.Fluent"/>
<StyleInclude Source="resm:Avalonia.Themes.Fluent.NativeMenuBar.xaml?assembly=Avalonia.Themes.Fluent"/>
<StyleInclude Source="resm:Avalonia.Themes.Fluent.ToggleSwitch.xaml?assembly=Avalonia.Themes.Fluent"/>
</Styles>

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

@ -1,16 +1,34 @@
<Style xmlns="https://github.com/avaloniaui" Selector="Menu">
<Style xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Selector="Menu">
<Design.PreviewWith>
<Border Padding="20">
<Menu>
<MenuItem Header="New" />
<MenuItem Header="Open" />
</Menu>
</Border>
</Design.PreviewWith>
<Style.Resources>
<x:Double x:Key="MenuHeight">32</x:Double>
</Style.Resources>
<Setter Property="Background" Value="Transparent" />
<Setter Property="Height" Value="{StaticResource MenuHeight}" />
<Setter Property="Template">
<ControlTemplate>
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
HorizontalAlignment="Stretch"
Padding="{TemplateBinding Padding}">
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
KeyboardNavigation.TabNavigation="Continue"/>
VerticalAlignment="Stretch"
KeyboardNavigation.TabNavigation="Continue" />
</Border>
</ControlTemplate>
</Setter>
</Style>
</Style>

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

@ -2,98 +2,143 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="clr-namespace:Avalonia.Controls.Converters;assembly=Avalonia.Controls"
xmlns:sys="clr-namespace:System;assembly=netstandard">
<Design.PreviewWith>
<Border Padding="20"
Width="400"
Height="200">
<Menu VerticalAlignment="Top">
<MenuItem Header="File">
<MenuItem Header="New"
InputGesture="Ctrl+N">
<MenuItem Header="XML" />
</MenuItem>
<MenuItem Header="Open">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsHitTestVisible="False"
IsChecked="True" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Exit"
InputGesture="Alt+F4" />
</MenuItem>
<MenuItem Header="Edit">
<MenuItem Header="Go To">
<MenuItem Header="Go To Line"/>
</MenuItem>
</MenuItem>
<MenuItem Header="View">
<MenuItem Header="Designer" InputGesture="Shift+F7" />
</MenuItem>
<MenuItem Header="Project">
<MenuItem Header="Add class" />
</MenuItem>
</Menu>
</Border>
</Design.PreviewWith>
<Styles.Resources>
<conv:PlatformKeyGestureConverter x:Key="KeyGestureConverter"/>
<conv:PlatformKeyGestureConverter x:Key="KeyGestureConverter" />
<Thickness x:Key="MenuFlyoutScrollerMargin">0,4,0,4</Thickness>
<Thickness x:Key="MenuIconPresenterMargin">0,0,12,0</Thickness>
<Thickness x:Key="MenuInputGestureTextMargin">24,0,0,0</Thickness>
<StreamGeometry x:Key="MenuItemChevronPathData">M 1,0 10,10 l -9,10 -1,-1 L 8,10 -0,1 Z</StreamGeometry>
</Styles.Resources>
<Style Selector="MenuItem">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="6 0"/>
<Setter Property="Background" Value="{DynamicResource MenuFlyoutItemBackground}" />
<Setter Property="BorderBrush" Value="{DynamicResource MenuFlyoutItemRevealBorderBrush}" />
<Setter Property="Foreground" Value="{DynamicResource MenuFlyoutItemForeground}" />
<Setter Property="Padding" Value="{DynamicResource MenuFlyoutItemThemePadding}" />
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="Template">
<ControlTemplate>
<Border Name="root"
<Border Name="PART_LayoutRoot"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20"/>
<ColumnDefinition Width="5"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto" SharedSizeGroup="MenuItemIGT"/>
<ColumnDefinition Width="20"/>
<ColumnDefinition Width="Auto"
SharedSizeGroup="MenuItemIcon" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto"
SharedSizeGroup="MenuItemIGT" />
<ColumnDefinition Width="Auto"
SharedSizeGroup="MenuItemChevron" />
</Grid.ColumnDefinitions>
<ContentPresenter Name="icon"
<ContentPresenter Name="PART_IconPresenter"
Content="{TemplateBinding Icon}"
Width="16"
Height="16"
Margin="3"
Margin="{DynamicResource MenuIconPresenterMargin}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
<Path Name="check"
Fill="{TemplateBinding Foreground}"
Data="F1M10,1.2L4.7,9.1 4.5,9.1 0,5.2 1.3,3.5 4.3,6.1 8.3,0 10,1.2z"
IsVisible="False"
Margin="3"
VerticalAlignment="Center"/>
<ContentPresenter Name="PART_HeaderPresenter"
Content="{TemplateBinding Header}"
Margin="{TemplateBinding Padding}"
VerticalAlignment="Center"
Grid.Column="2">
HorizontalAlignment="Stretch"
TextBlock.Foreground="{TemplateBinding Foreground}"
Grid.Column="1">
<ContentPresenter.DataTemplates>
<DataTemplate DataType="sys:String">
<AccessText Text="{Binding}"/>
<AccessText Text="{Binding}" />
</DataTemplate>
</ContentPresenter.DataTemplates>
</ContentPresenter>
<TextBlock x:Name="PART_InputGestureText"
Grid.Column="3"
Text="{TemplateBinding InputGesture, Converter={StaticResource KeyGestureConverter}}"
VerticalAlignment="Center"/>
<Path Name="rightArrow"
Data="M0,0L4,3.5 0,7z"
Fill="{DynamicResource ThemeForegroundBrush}"
Margin="10,0,0,0"
Grid.Column="2"
Classes="CaptionTextBlockStyle"
Margin="{DynamicResource MenuInputGestureTextMargin}"
Text="{TemplateBinding InputGesture,
Converter={StaticResource KeyGestureConverter}}"
HorizontalAlignment="Right"
VerticalAlignment="Center" />
<Path Name="PART_ChevronPath"
Stretch="Uniform"
Width="8"
Height="16"
Data="{StaticResource MenuItemChevronPathData}"
Margin="{DynamicResource MenuFlyoutItemChevronMargin}"
VerticalAlignment="Center"
Grid.Column="4"/>
Grid.Column="3" />
<Popup Name="PART_Popup"
WindowManagerAddShadowHint="True"
PlacementMode="Right"
StaysOpen="True"
IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}">
<Border Background="{TemplateBinding Background}"
BorderBrush="{DynamicResource ThemeBorderMidBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ScrollViewer>
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
Grid.IsSharedSizeScope="True"/>
IsOpen="{TemplateBinding IsSubMenuOpen,
Mode=TwoWay}">
<Border Background="{DynamicResource MenuFlyoutPresenterBackground}"
BorderBrush="{DynamicResource MenuFlyoutPresenterBorderBrush}"
BorderThickness="{DynamicResource MenuFlyoutPresenterBorderThemeThickness}"
Padding="{DynamicResource MenuFlyoutPresenterThemePadding}"
MaxWidth="{DynamicResource FlyoutThemeMaxWidth}"
MinHeight="{DynamicResource MenuFlyoutThemeMinHeight}"
HorizontalAlignment="Stretch"
CornerRadius="{DynamicResource OverlayCornerRadius}">
<ScrollViewer>
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
Margin="{DynamicResource MenuFlyoutScrollerMargin}"
Grid.IsSharedSizeScope="True" />
</ScrollViewer>
</Border>
</Popup>
</Grid>
</Grid>
</Border>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="MenuItem:separator">
<Setter Property="Template">
<ControlTemplate>
<Separator Background="{DynamicResource ThemeControlMidBrush}"
Margin="20,1,0,1"
Height="1"/>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="Menu > MenuItem">
<Setter Property="Padding" Value="6 0"/>
<Setter Property="Template">
<ControlTemplate>
<Border Name="root"
<Border Name="PART_LayoutRoot"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
@ -103,22 +148,31 @@
Margin="{TemplateBinding Padding}">
<ContentPresenter.DataTemplates>
<DataTemplate DataType="sys:String">
<AccessText Text="{Binding}"/>
<AccessText Text="{Binding}" />
</DataTemplate>
</ContentPresenter.DataTemplates>
</ContentPresenter>
<Popup Name="PART_Popup"
IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}"
WindowManagerAddShadowHint="False"
MinWidth="{Binding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}"
IsOpen="{TemplateBinding IsSubMenuOpen,
Mode=TwoWay}"
StaysOpen="True">
<Border Background="{TemplateBinding Background}"
BorderBrush="{DynamicResource ThemeBorderMidBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Border Background="{DynamicResource MenuFlyoutPresenterBackground}"
BorderBrush="{DynamicResource MenuFlyoutPresenterBorderBrush}"
BorderThickness="{DynamicResource MenuFlyoutPresenterBorderThemeThickness}"
Padding="{DynamicResource MenuFlyoutPresenterThemePadding}"
MaxWidth="{DynamicResource FlyoutThemeMaxWidth}"
MinHeight="{DynamicResource MenuFlyoutThemeMinHeight}"
HorizontalAlignment="Stretch"
CornerRadius="{DynamicResource OverlayCornerRadius}">
<ScrollViewer>
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
Grid.IsSharedSizeScope="True"/>
Margin="{DynamicResource MenuFlyoutScrollerMargin}"
Grid.IsSharedSizeScope="True" />
</ScrollViewer>
</Border>
</Popup>
@ -128,20 +182,78 @@
</Setter>
</Style>
<Style Selector="MenuItem /template/ ItemsPresenter#PART_ItemsPresenter">
<Setter Property="Margin" Value="2"/>
<Style Selector="MenuItem">
<!-- Narrow padding should be used for mouse input, when non-narrow one should be used for touch input in future. -->
<Setter Property="Padding" Value="{DynamicResource MenuFlyoutItemThemePaddingNarrow}" />
</Style>
<Style Selector="MenuItem /template/ ContentPresenter#PART_IconPresenter">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="MenuItem:icon /template/ ContentPresenter#PART_IconPresenter">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="MenuItem /template/ TextBlock#PART_InputGestureText">
<Setter Property="Foreground" Value="{DynamicResource MenuFlyoutItemKeyboardAcceleratorTextForeground}" />
</Style>
<Style Selector="MenuItem /template/ Path#PART_ChevronPath">
<Setter Property="Fill" Value="{DynamicResource MenuFlyoutSubItemChevron}" />
</Style>
<Style Selector="MenuItem:selected /template/ Border#root">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeAccentBrush}"/>
<Style Selector="MenuItem:selected /template/ Border#PART_LayoutRoot">
<Setter Property="Background" Value="{DynamicResource MenuFlyoutItemBackgroundPointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource MenuFlyoutItemBorderBrushPointerOver}" />
</Style>
<Style Selector="MenuItem:selected /template/ TextBlock#PART_InputGestureText">
<Setter Property="Foreground" Value="{DynamicResource MenuFlyoutItemKeyboardAcceleratorTextForegroundPointerOver}" />
</Style>
<Style Selector="MenuItem:selected /template/ Path#PART_ChevronPath">
<Setter Property="Fill" Value="{DynamicResource MenuFlyoutSubItemChevronPointerOver}" />
</Style>
<Style Selector="MenuItem:empty /template/ Path#rightArrow">
<Setter Property="IsVisible" Value="False"/>
<Style Selector="MenuItem:pressed /template/ Border#PART_LayoutRoot">
<Setter Property="Background" Value="{DynamicResource MenuFlyoutItemBackgroundPressed}" />
<Setter Property="BorderBrush" Value="{DynamicResource MenuFlyoutItemBorderBrushPressed}" />
</Style>
<Style Selector="MenuItem:pressed /template/ TextBlock#PART_InputGestureText">
<Setter Property="Foreground" Value="{DynamicResource MenuFlyoutItemKeyboardAcceleratorTextForegroundPressed}" />
</Style>
<Style Selector="MenuItem:pressed /template/ Path#PART_ChevronPath">
<Setter Property="Fill" Value="{DynamicResource MenuFlyoutSubItemChevronPressed}" />
</Style>
<Style Selector="MenuItem:disabled">
<Setter Property="Opacity" Value="{DynamicResource ThemeDisabledOpacity}"/>
<Setter Property="Background" Value="{DynamicResource MenuFlyoutItemBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource MenuFlyoutItemBorderBrushDisabled}" />
</Style>
<Style Selector="MenuItem:disabled /template/ TextBlock#PART_InputGestureText">
<Setter Property="Foreground" Value="{DynamicResource MenuFlyoutItemKeyboardAcceleratorTextForegroundDisabled}" />
</Style>
<Style Selector="MenuItem:disabled /template/ Path#PART_ChevronPath">
<Setter Property="Fill" Value="{DynamicResource MenuFlyoutSubItemChevronDisabled}" />
</Style>
<Style Selector="MenuItem:open /template/ Path#PART_ChevronPath">
<Setter Property="Fill" Value="{DynamicResource MenuFlyoutSubItemChevronSubMenuOpened}" />
</Style>
<Style Selector="MenuItem:empty /template/ Path#PART_ChevronPath">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="MenuItem:separator">
<Setter Property="Template">
<ControlTemplate>
<Separator />
</ControlTemplate>
</Setter>
</Style>
<Style Selector="MenuItem > Separator, ContextMenu > Separator">
<Setter Property="Background" Value="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Margin" Value="{DynamicResource MenuFlyoutSeparatorThemePadding}" />
<Setter Property="Height" Value="{DynamicResource MenuFlyoutSeparatorThemeHeight}" />
</Style>
</Styles>

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

@ -1,38 +1,60 @@
<Styles xmlns="https://github.com/avaloniaui">
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Design.PreviewWith>
<Border Padding="20"
Background="Black">
<StackPanel Spacing="20">
<NumericUpDown Minimum="0"
Maximum="10"
Increment="0.5"
Width="150"
Watermark="Enter text" />
<NumericUpDown Minimum="0"
Maximum="10"
Increment="0.5"
Width="150"
ButtonSpinnerLocation="Left"
Watermark="Enter text" />
</StackPanel>
</Border>
</Design.PreviewWith>
<Style Selector="NumericUpDown">
<Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
<Setter Property="BorderThickness" Value="{DynamicResource ThemeBorderThickness}"/>
<Setter Property="Padding" Value="4"/>
<Setter Property="Foreground" Value="{DynamicResource TextControlForeground}" />
<Setter Property="Background" Value="{DynamicResource TextControlBackground}" />
<Setter Property="BorderThickness" Value="{DynamicResource TextControlBorderThemeThickness}" />
<Setter Property="BorderBrush" Value="{DynamicResource TextControlBorderBrush}" />
<Setter Property="MinHeight" Value="{DynamicResource TextControlThemeMinHeight}" />
<Setter Property="MinWidth" Value="{DynamicResource TextControlThemeMinWidth}" />
<Setter Property="FontFamily" Value="{DynamicResource ContentControlThemeFontFamily}" />
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="Padding" Value="{DynamicResource TextControlThemePadding}" />
<Setter Property="Template">
<ControlTemplate>
<ButtonSpinner Name="PART_Spinner"
Background="{TemplateBinding Background}"
BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}"
Padding="0"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
AllowSpin="{TemplateBinding AllowSpin}"
ShowButtonSpinner="{TemplateBinding ShowButtonSpinner}"
ButtonSpinnerLocation="{TemplateBinding ButtonSpinnerLocation}">
<TextBox Name="PART_TextBox"
BorderThickness="0"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
Background="Transparent"
BorderBrush="Transparent"
Margin="-2"
Padding="{TemplateBinding Padding}"
Watermark="{TemplateBinding Watermark}"
DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}"
IsReadOnly="{TemplateBinding IsReadOnly}"
Text="{TemplateBinding Text}"
AcceptsReturn="False"
TextWrapping="NoWrap">
</TextBox>
TextWrapping="NoWrap" />
</ButtonSpinner>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="NumericUpDown /template/ TextBox#PART_TextBox">
<Setter Property="Margin" Value="4"/>
<Setter Property="MinWidth" Value="20"/>
</Style>
</Styles>
</Styles>

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

@ -54,7 +54,7 @@
<Setter Property="TextBlock.Foreground" Value="{DynamicResource RepeatButtonForegroundPressed}" />
</Style>
<Style Selector="RepeatButton:disabled">
<Style Selector="RepeatButton:disabled /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource RepeatButtonBackgroundDisabled}" />
<Setter Property="BorderBrush" Value="{DynamicResource RepeatButtonBorderBrushDisabled}" />
<Setter Property="TextBlock.Foreground" Value="{DynamicResource RepeatButtonForegroundDisabled}" />

12
src/Avalonia.Themes.Fluent/Separator.xaml

@ -4,17 +4,15 @@
<Setter Property="Focusable" Value="False"/>
<Setter Property="Template">
<ControlTemplate>
<Border BorderBrush="{TemplateBinding BorderBrush}"
<Border Padding="{TemplateBinding Margin}"
Height="{TemplateBinding Height}"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"/>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="MenuItem > Separator, ContextMenu > Separator">
<Setter Property="Background" Value="{DynamicResource ThemeControlMidBrush}"/>
<Setter Property="Margin" Value="29,1,0,1"/>
<Setter Property="Height" Value="1"/>
</Style>
</Styles>

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

@ -27,6 +27,7 @@
Grid.ColumnSpan="2"
TextBlock.FontWeight="Normal"
TextBlock.Foreground="{DynamicResource TextControlHeaderForeground}"
IsVisible="False"
Margin="{DynamicResource TextBoxTopHeaderMargin}" />
<Border Name="border"

1
src/Avalonia.Themes.Fluent/ToggleButton.xaml

@ -40,7 +40,6 @@
</Setter>
</Style>
<!-- PointerOverState -->
<Style Selector="ToggleButton:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource ToggleButtonBackgroundPointerOver}" />
<Setter Property="BorderBrush" Value="{DynamicResource ToggleButtonBorderBrushPointerOver}" />

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

@ -0,0 +1,294 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=netstandard">
<Styles.Resources>
<Thickness x:Key="ToggleSwitchTopHeaderMargin">0,0,0,6</Thickness>
<x:Double x:Key="ToggleSwitchPreContentMargin">6</x:Double>
<x:Double x:Key="ToggleSwitchPostContentMargin">6</x:Double>
<x:Double x:Key="ToggleSwitchThemeMinWidth">154</x:Double>
<x:Double x:Key="KnobOnPosition">20</x:Double>
<x:Double x:Key="KnobOffPosition">0</x:Double>
</Styles.Resources>
<Design.PreviewWith>
<StackPanel Margin="20" Width="250" Spacing="24" >
<StackPanel Spacing="12" >
<TextBlock
Text="Automatic updates"
Classes="h1"/>
<TextBlock
Text="Updates will be automaticly Downloaded and installed shile the computer is shutting down or restarting"
TextWrapping="Wrap"/>
<ToggleSwitch HorizontalContentAlignment="Left"
Content="Enable automatic Updates?"
OffContent="Uit"
OnContent="Aan"
VerticalAlignment="Bottom"/>
</StackPanel>
<StackPanel Spacing="12">
<TextBlock
Text="Previewer"
Classes="h1"/>
<TextBlock
Text="The previewer Shows a preview off your code, this could slow down your system"
TextWrapping="Wrap"/>
<ToggleSwitch
Content="Previewer"
IsChecked="True" />
</StackPanel>
</StackPanel>
</Design.PreviewWith>
<Style Selector="ToggleSwitch">
<Setter Property="Foreground" Value="{DynamicResource ToggleSwitchContentForeground}" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="FontFamily" Value="{DynamicResource ContentControlThemeFontFamily}" />
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="Template">
<ControlTemplate>
<Grid Background="{TemplateBinding Background}"
RowDefinitions="Auto,*">
<ContentPresenter x:Name="PART_ContentPresenter"
Grid.Row="0"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
Margin="{DynamicResource ToggleSwitchTopHeaderMargin}"
VerticalAlignment="Top"/>
<Grid Grid.Row="1"
MinWidth="{StaticResource ToggleSwitchThemeMinWidth}"
HorizontalAlignment="Left"
VerticalAlignment="Top">
<Grid.RowDefinitions>
<RowDefinition Height="{DynamicResource ToggleSwitchPreContentMargin}" />
<RowDefinition Height="Auto" />
<RowDefinition Height="{DynamicResource ToggleSwitchPostContentMargin}" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="12" MaxWidth="12" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid x:Name="SwitchAreaGrid"
Grid.RowSpan="3"
Grid.ColumnSpan="3"
TemplatedControl.IsTemplateFocusTarget="True"
Margin="0,5" />
<ContentPresenter x:Name="PART_OffContentPresenter"
Grid.RowSpan="3"
Grid.Column="2"
Content="{TemplateBinding OffContent}"
ContentTemplate="{TemplateBinding OffContentTemplate}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
<ContentPresenter x:Name="PART_OnContentPresenter"
Grid.RowSpan="3"
Grid.Column="2"
Content="{TemplateBinding OnContent}"
ContentTemplate="{TemplateBinding OnContentTemplate}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/>
<Border x:Name="OuterBorder"
Grid.Row="1"
Height="20"
Width="40"
CornerRadius="10"
BorderThickness="{DynamicResource ToggleSwitchOuterBorderStrokeThickness}" />
<Border x:Name="SwitchKnobBounds"
Grid.Row="1"
Height="20"
Width="40"
CornerRadius="10"
BorderThickness="{DynamicResource ToggleSwitchOnStrokeThickness}"/>
<Canvas x:Name="SwitchKnob" Grid.Row="1"
HorizontalAlignment="Left"
Width="20" Height="20">
<Grid x:Name="MovingKnobs"
Width="20" Height="20">
<Ellipse x:Name="SwitchKnobOn"
Width="10" Height="10" />
<Ellipse x:Name="SwitchKnobOff"
Width="10" Height="10" />
</Grid>
</Canvas>
</Grid>
</Grid>
</ControlTemplate>
</Setter>
</Style>
<!-- NormalState -->
<Style Selector="ToggleSwitch /template/ Grid#SwitchAreaGrid">
<Setter Property="Background" Value="{DynamicResource ToggleSwitchContainerBackground}"/>
</Style>
<Style Selector="ToggleSwitch /template/ Border#OuterBorder">
<Setter Property="Background" Value="{DynamicResource ToggleSwitchFillOff}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ToggleSwitchStrokeOff}"/>
</Style>
<Style Selector="ToggleSwitch /template/ Border#SwitchKnobBounds">
<Setter Property="Background" Value="{DynamicResource ToggleSwitchFillOn}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ToggleSwitchStrokeOn}"/>
</Style>
<Style Selector="ToggleSwitch /template/ Ellipse#SwitchKnobOn">
<Setter Property="Fill" Value="{DynamicResource ToggleSwitchKnobFillOn}"/>
<Setter Property="Stroke" Value="{DynamicResource ToggleSwitchKnobStrokeOn}"/>
</Style>
<Style Selector="ToggleSwitch /template/ Ellipse#SwitchKnobOff">
<Setter Property="Fill" Value="{DynamicResource ToggleSwitchKnobFillOff}"/>
<Setter Property="Stroke" Value="{DynamicResource ToggleSwitchKnobStrokeOff}"/>
</Style>
<Style Selector="ToggleSwitch /template/ Grid#MovingKnobs">
<Setter Property="Canvas.Left" Value="{DynamicResource KnobOffPosition}"/>
<Setter Property="Transitions">
<Transitions>
<DoubleTransition Property="Canvas.Left" Duration="0:0:0.2" Easing="CubicEaseOut"/>
</Transitions>
</Setter>
</Style>
<!-- PointerOverState -->
<Style Selector="ToggleSwitch:pointerover /template/ Border#OuterBorder">
<Setter Property="BorderBrush" Value="{DynamicResource ToggleSwitchStrokeOffPointerOver}"/>
<Setter Property="Background" Value="{DynamicResource ToggleSwitchFillOffPointerOver}"/>
</Style>
<Style Selector="ToggleSwitch:pointerover /template/ Ellipse#SwitchKnobOff">
<Setter Property="Fill" Value="{DynamicResource ToggleSwitchKnobFillOffPointerOver}"/>
</Style>
<Style Selector="ToggleSwitch:pointerover /template/ Ellipse#SwitchKnobOn">
<Setter Property="Fill" Value="{DynamicResource ToggleSwitchKnobFillOnPointerOver}"/>
</Style>
<Style Selector="ToggleSwitch:pointerover /template/ Border#SwitchKnobBounds">
<Setter Property="Background" Value="{DynamicResource ToggleSwitchFillOnPointerOver}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ToggleSwitchStrokeOnPointerOver}"/>
</Style>
<Style Selector="ToggleSwitch:pointerover /template/ Grid#SwitchAreaGrid">
<Setter Property="Background" Value="{DynamicResource ToggleSwitchContainerBackgroundPointerOver}"/>
</Style>
<!-- PressedState -->
<Style Selector="ToggleSwitch:pressed /template/ Border#OuterBorder">
<Setter Property="BorderBrush" Value="{DynamicResource ToggleSwitchStrokeOffPressed}"/>
<Setter Property="Background" Value="{DynamicResource ToggleSwitchFillOffPressed}"/>
</Style>
<Style Selector="ToggleSwitch:pressed /template/ Border#SwitchKnobBounds">
<Setter Property="Background" Value="{DynamicResource ToggleSwitchFillOnPressed}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ToggleSwitchStrokeOnPressed}"/>
</Style>
<Style Selector="ToggleSwitch:pressed /template/ Ellipse#SwitchKnobOff">
<Setter Property="Fill" Value="{DynamicResource ToggleSwitchKnobFillOffPressed}"/>
</Style>
<Style Selector="ToggleSwitch:pressed /template/ Ellipse#SwitchKnobOn">
<Setter Property="Fill" Value="{DynamicResource ToggleSwitchKnobFillOnPressed}"/>
</Style>
<Style Selector="ToggleSwitch:pressed /template/ Grid#SwitchAreaGrid">
<Setter Property="Background" Value="{DynamicResource ToggleSwitchContainerBackgroundPressed}"/>
</Style>
<!-- DisabledState -->
<Style Selector="ToggleSwitch:disabled">
<Setter Property="Foreground" Value="{DynamicResource ToggleSwitchHeaderForegroundDisabled}"/>
</Style>
<Style Selector="ToggleSwitch:disabled /template/ Border#OuterBorder">
<Setter Property="BorderBrush" Value="{DynamicResource ToggleSwitchStrokeOffDisabled}"/>
<Setter Property="Background" Value="{DynamicResource ToggleSwitchFillOffPressed}"/>
</Style>
<Style Selector="ToggleSwitch:disabled /template/ Ellipse#SwitchKnobOff">
<Setter Property="Fill" Value="{DynamicResource ToggleSwitchKnobFillOffDisabled}"/>
</Style>
<Style Selector="ToggleSwitch:disabled /template/ Ellipse#SwitchKnobOn">
<Setter Property="Fill" Value="{DynamicResource ToggleSwitchKnobFillOnDisabled}"/>
</Style>
<Style Selector="ToggleSwitch:disabled /template/ Border#SwitchKnobBounds">
<Setter Property="Background" Value="{DynamicResource ToggleSwitchFillOnDisabled}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ToggleSwitchStrokeOnDisabled}"/>
</Style>
<!-- CheckedState -->
<Style Selector="ToggleSwitch:checked /template/ Grid#MovingKnobs">
<Setter Property="Canvas.Left" Value="{DynamicResource KnobOnPosition}"/>
</Style>
<Style Selector="ToggleSwitch:checked /template/ Border#OuterBorder">
<Setter Property="Opacity" Value="0"/>
</Style>
<Style Selector="ToggleSwitch:checked /template/ Ellipse#SwitchKnobOff">
<Setter Property="Opacity" Value="0"/>
</Style>
<Style Selector="ToggleSwitch:checked /template/ Border#SwitchKnobBounds">
<Setter Property="Opacity" Value="1"/>
</Style>
<Style Selector="ToggleSwitch:checked /template/ Ellipse#SwitchKnobOn">
<Setter Property="Opacity" Value="1"/>
</Style>
<Style Selector="ToggleSwitch:checked /template/ ContentPresenter#PART_OffContentPresenter">
<Setter Property="Opacity" Value="0"/>
</Style>
<Style Selector="ToggleSwitch:checked /template/ ContentPresenter#PART_OnContentPresenter">
<Setter Property="Opacity" Value="1"/>
</Style>
<!--UncheckedState -->
<Style Selector="ToggleSwitch:unchecked /template/ Grid#MovingKnobs">
<Setter Property="Canvas.Left" Value="{DynamicResource KnobOffPosition}"/>
</Style>
<Style Selector="ToggleSwitch:unchecked /template/ Border#OuterBorder">
<Setter Property="Opacity" Value="1"/>
</Style>
<Style Selector="ToggleSwitch:unchecked /template/ Ellipse#SwitchKnobOff">
<Setter Property="Opacity" Value="1"/>
</Style>
<Style Selector="ToggleSwitch:unchecked /template/ Ellipse#SwitchKnobOn">
<Setter Property="Opacity" Value="0"/>
</Style>
<Style Selector="ToggleSwitch:unchecked /template/ Border#SwitchKnobBounds">
<Setter Property="Opacity" Value="0"/>
</Style>
<Style Selector="ToggleSwitch:unchecked /template/ ContentPresenter#PART_OffContentPresenter">
<Setter Property="Opacity" Value="1"/>
</Style>
<Style Selector="ToggleSwitch:unchecked /template/ ContentPresenter#PART_OnContentPresenter">
<Setter Property="Opacity" Value="0"/>
</Style>
</Styles>

10
src/Avalonia.Visuals/Animation/Animators/TransformAnimator.cs

@ -1,6 +1,8 @@
using System;
using System.Reactive.Disposables;
using Avalonia.Logging;
using Avalonia.Media;
using Avalonia.Media.Transformation;
namespace Avalonia.Animation.Animators
{
@ -19,6 +21,12 @@ namespace Avalonia.Animation.Animators
// Check if the Target Property is Transform derived.
if (typeof(Transform).IsAssignableFrom(Property.OwnerType))
{
if (ctrl.RenderTransform is TransformOperations)
{
// HACK: This animator cannot reasonably animate CSS transforms at the moment.
return Disposable.Empty;
}
if (ctrl.RenderTransform == null)
{
var normalTransform = new TransformGroup();
@ -51,7 +59,7 @@ namespace Avalonia.Animation.Animators
// It's a transform object so let's target that.
if (renderTransformType == Property.OwnerType)
{
return _doubleAnimator.Apply(animation, ctrl.RenderTransform, clock ?? control.Clock, obsMatch, onComplete);
return _doubleAnimator.Apply(animation, (Transform) ctrl.RenderTransform, clock ?? control.Clock, obsMatch, onComplete);
}
// It's a TransformGroup and try finding the target there.
else if (renderTransformType == typeof(TransformGroup))

35
src/Avalonia.Visuals/Animation/Animators/TransformOperationsAnimator.cs

@ -0,0 +1,35 @@
using System;
using Avalonia.Media;
using Avalonia.Media.Transformation;
namespace Avalonia.Animation.Animators
{
public class TransformOperationsAnimator : Animator<TransformOperations>
{
public TransformOperationsAnimator()
{
Validate = ValidateTransform;
}
public override TransformOperations Interpolate(double progress, TransformOperations oldValue, TransformOperations newValue)
{
var oldTransform = EnsureOperations(oldValue);
var newTransform = EnsureOperations(newValue);
return TransformOperations.Interpolate(oldTransform, newTransform, progress);
}
internal static TransformOperations EnsureOperations(ITransform value)
{
return value as TransformOperations ?? TransformOperations.Identity;
}
private void ValidateTransform(AnimatorKeyFrame kf)
{
if (!(kf.Value is TransformOperations))
{
throw new InvalidOperationException($"All keyframes must be of type {typeof(TransformOperations)}.");
}
}
}
}

28
src/Avalonia.Visuals/Animation/Transitions/TransformOperationsTransition.cs

@ -0,0 +1,28 @@
using System;
using System.Reactive.Linq;
using Avalonia.Animation.Animators;
using Avalonia.Media;
namespace Avalonia.Animation
{
public class TransformOperationsTransition : Transition<ITransform>
{
private static readonly TransformOperationsAnimator _operationsAnimator = new TransformOperationsAnimator();
public override IObservable<ITransform> DoTransition(IObservable<double> progress,
ITransform oldValue,
ITransform newValue)
{
var oldTransform = TransformOperationsAnimator.EnsureOperations(oldValue);
var newTransform = TransformOperationsAnimator.EnsureOperations(newValue);
return progress
.Select(p =>
{
var f = Easing.Ease(p);
return _operationsAnimator.Interpolate(f, oldTransform, newTransform);
});
}
}
}

75
src/Avalonia.Visuals/Matrix.cs

@ -54,7 +54,7 @@ namespace Avalonia
/// <summary>
/// HasInverse Property - returns true if this matrix is invertible, false otherwise.
/// </summary>
public bool HasInverse => GetDeterminant() != 0;
public bool HasInverse => !MathUtilities.IsZero(GetDeterminant());
/// <summary>
/// The first element of the first row
@ -286,7 +286,7 @@ namespace Avalonia
{
double d = GetDeterminant();
if (d == 0)
if (MathUtilities.IsZero(d))
{
throw new InvalidOperationException("Transform is not invertible.");
}
@ -319,5 +319,76 @@ namespace Avalonia
);
}
}
/// <summary>
/// Decomposes given matrix into transform operations.
/// </summary>
/// <param name="matrix">Matrix to decompose.</param>
/// <param name="decomposed">Decomposed matrix.</param>
/// <returns>The status of the operation.</returns>
public static bool TryDecomposeTransform(Matrix matrix, out Decomposed decomposed)
{
decomposed = default;
var determinant = matrix.GetDeterminant();
if (MathUtilities.IsZero(determinant))
{
return false;
}
var m11 = matrix.M11;
var m21 = matrix.M21;
var m12 = matrix.M12;
var m22 = matrix.M22;
// Translation.
decomposed.Translate = new Vector(matrix.M31, matrix.M32);
// Scale sign.
var scaleX = 1d;
var scaleY = 1d;
if (determinant < 0)
{
if (m11 < m22)
{
scaleX *= -1d;
}
else
{
scaleY *= -1d;
}
}
// X Scale.
scaleX *= Math.Sqrt(m11 * m11 + m12 * m12);
m11 /= scaleX;
m12 /= scaleX;
// XY Shear.
double scaledShear = m11 * m21 + m12 * m22;
m21 -= m11 * scaledShear;
m22 -= m12 * scaledShear;
// Y Scale.
scaleY *= Math.Sqrt(m21 * m21 + m22 * m22);
decomposed.Scale = new Vector(scaleX, scaleY);
decomposed.Skew = new Vector(scaledShear / scaleY, 0d);
decomposed.Angle = Math.Atan2(m12, m11);
return true;
}
public struct Decomposed
{
public Vector Translate;
public Vector Scale;
public Vector Skew;
public double Angle;
}
}
}

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

@ -89,33 +89,64 @@ namespace Avalonia.Media
/// <returns>The <see cref="Color"/>.</returns>
public static Color Parse(string s)
{
if (s == null) throw new ArgumentNullException(nameof(s));
if (s.Length == 0) throw new FormatException();
if (TryParse(s, out Color color))
{
return color;
}
if (s[0] == '#')
throw new FormatException($"Invalid color string: '{s}'.");
}
/// <summary>
/// Parses a color string.
/// </summary>
/// <param name="s">The color string.</param>
/// <returns>The <see cref="Color"/>.</returns>
public static Color Parse(ReadOnlySpan<char> s)
{
if (TryParse(s, out Color color))
{
var or = 0u;
return color;
}
if (s.Length == 7)
{
or = 0xff000000;
}
else if (s.Length != 9)
{
throw new FormatException($"Invalid color string: '{s}'.");
}
throw new FormatException($"Invalid color string: '{s.ToString()}'.");
}
return FromUInt32(uint.Parse(s.Substring(1), NumberStyles.HexNumber, CultureInfo.InvariantCulture) | or);
/// <summary>
/// Parses a color string.
/// </summary>
/// <param name="s">The color string.</param>
/// <param name="color">The parsed color</param>
/// <returns>The status of the operation.</returns>
public static bool TryParse(string s, out Color color)
{
if (s == null)
{
throw new ArgumentNullException(nameof(s));
}
if (s.Length == 0)
{
throw new FormatException();
}
if (s[0] == '#' && TryParseInternal(s.AsSpan(), out color))
{
return true;
}
var knownColor = KnownColors.GetKnownColor(s);
if (knownColor != KnownColor.None)
{
return knownColor.ToColor();
color = knownColor.ToColor();
return true;
}
throw new FormatException($"Invalid color string: '{s}'.");
color = default;
return false;
}
/// <summary>
@ -126,40 +157,79 @@ namespace Avalonia.Media
/// <returns>The status of the operation.</returns>
public static bool TryParse(ReadOnlySpan<char> s, out Color color)
{
color = default;
if (s == null)
return false;
if (s.Length == 0)
{
color = default;
return false;
}
if (s[0] == '#')
{
var or = 0u;
return TryParseInternal(s, out color);
}
var knownColor = KnownColors.GetKnownColor(s.ToString());
if (knownColor != KnownColor.None)
{
color = knownColor.ToColor();
return true;
}
color = default;
return false;
}
private static bool TryParseInternal(ReadOnlySpan<char> s, out Color color)
{
static bool TryParseCore(ReadOnlySpan<char> input, ref Color color)
{
var alphaComponent = 0u;
if (s.Length == 7)
if (input.Length == 6)
{
or = 0xff000000;
alphaComponent = 0xff000000;
}
else if (s.Length != 9)
else if (input.Length != 8)
{
return false;
}
if(!uint.TryParse(s.Slice(1).ToString(), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var parsed))
// TODO: (netstandard 2.1) Can use allocation free parsing.
if (!uint.TryParse(input.ToString(), NumberStyles.HexNumber, CultureInfo.InvariantCulture,
out var parsed))
{
return false;
color = FromUInt32(parsed| or);
}
color = FromUInt32(parsed | alphaComponent);
return true;
}
var knownColor = KnownColors.GetKnownColor(s.ToString());
color = default;
if (knownColor != KnownColor.None)
ReadOnlySpan<char> input = s.Slice(1);
// Handle shorthand cases like #FFF (RGB) or #FFFF (ARGB).
if (input.Length == 3 || input.Length == 4)
{
color = knownColor.ToColor();
return true;
var extendedLength = 2 * input.Length;
Span<char> extended = stackalloc char[extendedLength];
for (int i = 0; i < input.Length; i++)
{
extended[2 * i + 0] = input[i];
extended[2 * i + 1] = input[i];
}
return TryParseCore(extended, ref color);
}
return false;
return TryParseCore(input, ref color);
}
/// <summary>

12
src/Avalonia.Visuals/Media/IMutableTransform.cs

@ -0,0 +1,12 @@
using System;
namespace Avalonia.Media
{
public interface IMutableTransform : ITransform
{
/// <summary>
/// Raised when the transform changes.
/// </summary>
event EventHandler Changed;
}
}

10
src/Avalonia.Visuals/Media/ITransform.cs

@ -0,0 +1,10 @@
using System.ComponentModel;
namespace Avalonia.Media
{
[TypeConverter(typeof(TransformConverter))]
public interface ITransform
{
Matrix Value { get; }
}
}

5
src/Avalonia.Visuals/Media/Transform.cs

@ -8,11 +8,12 @@ namespace Avalonia.Media
/// <summary>
/// Represents a transform on an <see cref="IVisual"/>.
/// </summary>
public abstract class Transform : Animatable
public abstract class Transform : Animatable, IMutableTransform
{
static Transform()
{
Animation.Animation.RegisterAnimator<TransformAnimator>(prop => typeof(Transform).IsAssignableFrom(prop.OwnerType));
Animation.Animation.RegisterAnimator<TransformAnimator>(prop =>
typeof(ITransform).IsAssignableFrom(prop.OwnerType));
}
/// <summary>

23
src/Avalonia.Visuals/Media/TransformConverter.cs

@ -0,0 +1,23 @@
using System;
using System.ComponentModel;
using System.Globalization;
using Avalonia.Media.Transformation;
namespace Avalonia.Media
{
/// <summary>
/// Creates an <see cref="ITransform"/> from a string representation.
/// </summary>
public class TransformConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
return sourceType == typeof(string);
}
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
{
return TransformOperations.Parse((string)value);
}
}
}

40
src/Avalonia.Visuals/Media/Transformation/InterpolationUtilities.cs

@ -0,0 +1,40 @@
namespace Avalonia.Media.Transformation
{
internal static class InterpolationUtilities
{
public static double InterpolateScalars(double from, double to, double progress)
{
return from * (1d - progress) + to * progress;
}
public static Vector InterpolateVectors(Vector from, Vector to, double progress)
{
var x = InterpolateScalars(from.X, to.X, progress);
var y = InterpolateScalars(from.Y, to.Y, progress);
return new Vector(x, y);
}
public static Matrix ComposeTransform(Matrix.Decomposed decomposed)
{
// According to https://www.w3.org/TR/css-transforms-1/#recomposing-to-a-2d-matrix
return Matrix.CreateTranslation(decomposed.Translate) *
Matrix.CreateRotation(decomposed.Angle) *
Matrix.CreateSkew(decomposed.Skew.X, decomposed.Skew.Y) *
Matrix.CreateScale(decomposed.Scale);
}
public static Matrix.Decomposed InterpolateDecomposedTransforms(ref Matrix.Decomposed from, ref Matrix.Decomposed to, double progres)
{
Matrix.Decomposed result = default;
result.Translate = InterpolateVectors(from.Translate, to.Translate, progres);
result.Scale = InterpolateVectors(from.Scale, to.Scale, progres);
result.Skew = InterpolateVectors(from.Skew, to.Skew, progres);
result.Angle = InterpolateScalars(from.Angle, to.Angle, progres);
return result;
}
}
}

230
src/Avalonia.Visuals/Media/Transformation/TransformOperation.cs

@ -0,0 +1,230 @@
using System.Runtime.InteropServices;
namespace Avalonia.Media.Transformation
{
/// <summary>
/// Represents a single primitive transform (like translation, rotation, scale, etc.).
/// </summary>
public struct TransformOperation
{
public OperationType Type;
public Matrix Matrix;
public DataLayout Data;
public enum OperationType
{
Translate,
Rotate,
Scale,
Skew,
Matrix,
Identity
}
/// <summary>
/// Returns whether operation produces the identity matrix.
/// </summary>
public bool IsIdentity => Matrix.IsIdentity;
/// <summary>
/// Bakes this operation to a transform matrix.
/// </summary>
public void Bake()
{
Matrix = Matrix.Identity;
switch (Type)
{
case OperationType.Translate:
{
Matrix = Matrix.CreateTranslation(Data.Translate.X, Data.Translate.Y);
break;
}
case OperationType.Rotate:
{
Matrix = Matrix.CreateRotation(Data.Rotate.Angle);
break;
}
case OperationType.Scale:
{
Matrix = Matrix.CreateScale(Data.Scale.X, Data.Scale.Y);
break;
}
case OperationType.Skew:
{
Matrix = Matrix.CreateSkew(Data.Skew.X, Data.Skew.Y);
break;
}
}
}
/// <summary>
/// Returns new identity transform operation.
/// </summary>
public static TransformOperation Identity =>
new TransformOperation { Matrix = Matrix.Identity, Type = OperationType.Identity };
/// <summary>
/// Attempts to interpolate between two transform operations.
/// </summary>
/// <param name="from">Source operation.</param>
/// <param name="to">Target operation.</param>
/// <param name="progress">Interpolation progress.</param>
/// <param name="result">Interpolation result that will be filled in when operation was successful.</param>
/// <remarks>
/// Based upon https://www.w3.org/TR/css-transforms-1/#interpolation-of-transform-functions.
/// </remarks>
public static bool TryInterpolate(TransformOperation? from, TransformOperation? to, double progress,
ref TransformOperation result)
{
bool fromIdentity = IsOperationIdentity(ref from);
bool toIdentity = IsOperationIdentity(ref to);
if (fromIdentity && toIdentity)
{
return true;
}
// ReSharper disable PossibleInvalidOperationException
TransformOperation fromValue = fromIdentity ? Identity : from.Value;
TransformOperation toValue = toIdentity ? Identity : to.Value;
// ReSharper restore PossibleInvalidOperationException
var interpolationType = toIdentity ? fromValue.Type : toValue.Type;
result.Type = interpolationType;
switch (interpolationType)
{
case OperationType.Translate:
{
double fromX = fromIdentity ? 0 : fromValue.Data.Translate.X;
double fromY = fromIdentity ? 0 : fromValue.Data.Translate.Y;
double toX = toIdentity ? 0 : toValue.Data.Translate.X;
double toY = toIdentity ? 0 : toValue.Data.Translate.Y;
result.Data.Translate.X = InterpolationUtilities.InterpolateScalars(fromX, toX, progress);
result.Data.Translate.Y = InterpolationUtilities.InterpolateScalars(fromY, toY, progress);
result.Bake();
break;
}
case OperationType.Rotate:
{
double fromAngle = fromIdentity ? 0 : fromValue.Data.Rotate.Angle;
double toAngle = toIdentity ? 0 : toValue.Data.Rotate.Angle;
result.Data.Rotate.Angle = InterpolationUtilities.InterpolateScalars(fromAngle, toAngle, progress);
result.Bake();
break;
}
case OperationType.Scale:
{
double fromX = fromIdentity ? 1 : fromValue.Data.Scale.X;
double fromY = fromIdentity ? 1 : fromValue.Data.Scale.Y;
double toX = toIdentity ? 1 : toValue.Data.Scale.X;
double toY = toIdentity ? 1 : toValue.Data.Scale.Y;
result.Data.Scale.X = InterpolationUtilities.InterpolateScalars(fromX, toX, progress);
result.Data.Scale.Y = InterpolationUtilities.InterpolateScalars(fromY, toY, progress);
result.Bake();
break;
}
case OperationType.Skew:
{
double fromX = fromIdentity ? 0 : fromValue.Data.Skew.X;
double fromY = fromIdentity ? 0 : fromValue.Data.Skew.Y;
double toX = toIdentity ? 0 : toValue.Data.Skew.X;
double toY = toIdentity ? 0 : toValue.Data.Skew.Y;
result.Data.Skew.X = InterpolationUtilities.InterpolateScalars(fromX, toX, progress);
result.Data.Skew.Y = InterpolationUtilities.InterpolateScalars(fromY, toY, progress);
result.Bake();
break;
}
case OperationType.Matrix:
{
var fromMatrix = fromIdentity ? Matrix.Identity : fromValue.Matrix;
var toMatrix = toIdentity ? Matrix.Identity : toValue.Matrix;
if (!Matrix.TryDecomposeTransform(fromMatrix, out Matrix.Decomposed fromDecomposed) ||
!Matrix.TryDecomposeTransform(toMatrix, out Matrix.Decomposed toDecomposed))
{
return false;
}
var interpolated =
InterpolationUtilities.InterpolateDecomposedTransforms(
ref fromDecomposed, ref toDecomposed,
progress);
result.Matrix = InterpolationUtilities.ComposeTransform(interpolated);
break;
}
case OperationType.Identity:
{
// Do nothing.
break;
}
}
return true;
}
private static bool IsOperationIdentity(ref TransformOperation? operation)
{
return !operation.HasValue || operation.Value.IsIdentity;
}
[StructLayout(LayoutKind.Explicit)]
public struct DataLayout
{
[FieldOffset(0)] public SkewLayout Skew;
[FieldOffset(0)] public ScaleLayout Scale;
[FieldOffset(0)] public TranslateLayout Translate;
[FieldOffset(0)] public RotateLayout Rotate;
public struct SkewLayout
{
public double X;
public double Y;
}
public struct ScaleLayout
{
public double X;
public double Y;
}
public struct TranslateLayout
{
public double X;
public double Y;
}
public struct RotateLayout
{
public double Angle;
}
}
}
}

258
src/Avalonia.Visuals/Media/Transformation/TransformOperations.cs

@ -0,0 +1,258 @@
using System;
using System.Collections.Generic;
namespace Avalonia.Media.Transformation
{
/// <summary>
/// Contains a list of <see cref="TransformOperation"/> that represent primitive transforms that will be
/// applied in declared order.
/// </summary>
public sealed class TransformOperations : ITransform
{
public static TransformOperations Identity { get; } = new TransformOperations(new List<TransformOperation>());
private readonly List<TransformOperation> _operations;
private TransformOperations(List<TransformOperation> operations)
{
_operations = operations ?? throw new ArgumentNullException(nameof(operations));
IsIdentity = CheckIsIdentity();
Value = ApplyTransforms();
}
/// <summary>
/// Returns whether all operations combined together produce the identity matrix.
/// </summary>
public bool IsIdentity { get; }
public IReadOnlyList<TransformOperation> Operations => _operations;
public Matrix Value { get; }
public static TransformOperations Parse(string s)
{
return TransformParser.Parse(s);
}
public static Builder CreateBuilder(int capacity)
{
return new Builder(capacity);
}
public static TransformOperations Interpolate(TransformOperations from, TransformOperations to, double progress)
{
TransformOperations result = Identity;
if (!TryInterpolate(from, to, progress, ref result))
{
// If the matrices cannot be interpolated, fallback to discrete animation logic.
// See https://drafts.csswg.org/css-transforms/#matrix-interpolation
result = progress < 0.5 ? from : to;
}
return result;
}
private Matrix ApplyTransforms(int startOffset = 0)
{
Matrix matrix = Matrix.Identity;
for (var i = startOffset; i < _operations.Count; i++)
{
TransformOperation operation = _operations[i];
matrix *= operation.Matrix;
}
return matrix;
}
private bool CheckIsIdentity()
{
foreach (TransformOperation operation in _operations)
{
if (!operation.IsIdentity)
{
return false;
}
}
return true;
}
private static bool TryInterpolate(TransformOperations from, TransformOperations to, double progress, ref TransformOperations result)
{
bool fromIdentity = from.IsIdentity;
bool toIdentity = to.IsIdentity;
if (fromIdentity && toIdentity)
{
return true;
}
int matchingPrefixLength = ComputeMatchingPrefixLength(from, to);
int fromSize = fromIdentity ? 0 : from._operations.Count;
int toSize = toIdentity ? 0 : to._operations.Count;
int numOperations = Math.Max(fromSize, toSize);
var builder = new Builder(matchingPrefixLength);
for (int i = 0; i < matchingPrefixLength; i++)
{
TransformOperation interpolated = new TransformOperation
{
Type = TransformOperation.OperationType.Identity
};
if (!TransformOperation.TryInterpolate(
i >= fromSize ? default(TransformOperation?) : from._operations[i],
i >= toSize ? default(TransformOperation?) : to._operations[i],
progress,
ref interpolated))
{
return false;
}
builder.Append(interpolated);
}
if (matchingPrefixLength < numOperations)
{
if (!ComputeDecomposedTransform(from, matchingPrefixLength, out Matrix.Decomposed fromDecomposed) ||
!ComputeDecomposedTransform(to, matchingPrefixLength, out Matrix.Decomposed toDecomposed))
{
return false;
}
var transform = InterpolationUtilities.InterpolateDecomposedTransforms(ref fromDecomposed, ref toDecomposed, progress);
builder.AppendMatrix(InterpolationUtilities.ComposeTransform(transform));
}
result = builder.Build();
return true;
}
private static bool ComputeDecomposedTransform(TransformOperations operations, int startOffset, out Matrix.Decomposed decomposed)
{
Matrix transform = operations.ApplyTransforms(startOffset);
if (!Matrix.TryDecomposeTransform(transform, out decomposed))
{
return false;
}
return true;
}
private static int ComputeMatchingPrefixLength(TransformOperations from, TransformOperations to)
{
int numOperations = Math.Min(from._operations.Count, to._operations.Count);
for (int i = 0; i < numOperations; i++)
{
if (from._operations[i].Type != to._operations[i].Type)
{
return i;
}
}
// If the operations match to the length of the shorter list, then pad its
// length with the matching identity operations.
// https://drafts.csswg.org/css-transforms/#transform-function-lists
return Math.Max(from._operations.Count, to._operations.Count);
}
public readonly struct Builder
{
private readonly List<TransformOperation> _operations;
public Builder(int capacity)
{
_operations = new List<TransformOperation>(capacity);
}
public void AppendTranslate(double x, double y)
{
var toAdd = new TransformOperation();
toAdd.Type = TransformOperation.OperationType.Translate;
toAdd.Data.Translate.X = x;
toAdd.Data.Translate.Y = y;
toAdd.Bake();
_operations.Add(toAdd);
}
public void AppendRotate(double angle)
{
var toAdd = new TransformOperation();
toAdd.Type = TransformOperation.OperationType.Rotate;
toAdd.Data.Rotate.Angle = angle;
toAdd.Bake();
_operations.Add(toAdd);
}
public void AppendScale(double x, double y)
{
var toAdd = new TransformOperation();
toAdd.Type = TransformOperation.OperationType.Scale;
toAdd.Data.Scale.X = x;
toAdd.Data.Scale.Y = y;
toAdd.Bake();
_operations.Add(toAdd);
}
public void AppendSkew(double x, double y)
{
var toAdd = new TransformOperation();
toAdd.Type = TransformOperation.OperationType.Skew;
toAdd.Data.Skew.X = x;
toAdd.Data.Skew.Y = y;
toAdd.Bake();
_operations.Add(toAdd);
}
public void AppendMatrix(Matrix matrix)
{
var toAdd = new TransformOperation();
toAdd.Type = TransformOperation.OperationType.Matrix;
toAdd.Matrix = matrix;
_operations.Add(toAdd);
}
public void AppendIdentity()
{
var toAdd = new TransformOperation();
toAdd.Type = TransformOperation.OperationType.Identity;
_operations.Add(toAdd);
}
public void Append(TransformOperation toAdd)
{
_operations.Add(toAdd);
}
public TransformOperations Build()
{
return new TransformOperations(_operations);
}
}
}
}

444
src/Avalonia.Visuals/Media/Transformation/TransformParser.cs

@ -0,0 +1,444 @@
using System;
using System.Globalization;
using Avalonia.Utilities;
namespace Avalonia.Media.Transformation
{
public static class TransformParser
{
private static readonly (string, TransformFunction)[] s_functionMapping =
{
("translate", TransformFunction.Translate),
("translateX", TransformFunction.TranslateX),
("translateY", TransformFunction.TranslateY),
("scale", TransformFunction.Scale),
("scaleX", TransformFunction.ScaleX),
("scaleY", TransformFunction.ScaleY),
("skew", TransformFunction.Skew),
("skewX", TransformFunction.SkewX),
("skewY", TransformFunction.SkewY),
("rotate", TransformFunction.Rotate),
("matrix", TransformFunction.Matrix)
};
private static readonly (string, Unit)[] s_unitMapping =
{
("deg", Unit.Degree),
("grad", Unit.Gradian),
("rad", Unit.Radian),
("turn", Unit.Turn),
("px", Unit.Pixel)
};
public static TransformOperations Parse(string s)
{
void ThrowInvalidFormat()
{
throw new FormatException($"Invalid transform string: '{s}'.");
}
if (string.IsNullOrEmpty(s))
{
throw new ArgumentException(nameof(s));
}
var span = s.AsSpan().Trim();
if (span.Equals("none".AsSpan(), StringComparison.OrdinalIgnoreCase))
{
return TransformOperations.Identity;
}
var builder = TransformOperations.CreateBuilder(0);
while (true)
{
var beginIndex = span.IndexOf('(');
var endIndex = span.IndexOf(')');
if (beginIndex == -1 || endIndex == -1)
{
ThrowInvalidFormat();
}
var namePart = span.Slice(0, beginIndex).Trim();
var function = ParseTransformFunction(in namePart);
if (function == TransformFunction.Invalid)
{
ThrowInvalidFormat();
}
var valuePart = span.Slice(beginIndex + 1, endIndex - beginIndex - 1).Trim();
ParseFunction(in valuePart, function, in builder);
span = span.Slice(endIndex + 1);
if (span.IsWhiteSpace())
{
break;
}
}
return builder.Build();
}
private static void ParseFunction(
in ReadOnlySpan<char> functionPart,
TransformFunction function,
in TransformOperations.Builder builder)
{
static UnitValue ParseValue(ReadOnlySpan<char> part)
{
int unitIndex = -1;
for (int i = 0; i < part.Length; i++)
{
char c = part[i];
if (char.IsDigit(c) || c == '-' || c == '.')
{
continue;
}
unitIndex = i;
break;
}
Unit unit = Unit.None;
if (unitIndex != -1)
{
var unitPart = part.Slice(unitIndex, part.Length - unitIndex);
unit = ParseUnit(unitPart);
part = part.Slice(0, unitIndex);
}
var value = double.Parse(part.ToString(), NumberStyles.Float, CultureInfo.InvariantCulture);
return new UnitValue(unit, value);
}
static int ParseValuePair(
in ReadOnlySpan<char> part,
ref UnitValue leftValue,
ref UnitValue rightValue)
{
var commaIndex = part.IndexOf(',');
if (commaIndex != -1)
{
var leftPart = part.Slice(0, commaIndex).Trim();
var rightPart = part.Slice(commaIndex + 1, part.Length - commaIndex - 1).Trim();
leftValue = ParseValue(leftPart);
rightValue = ParseValue(rightPart);
return 2;
}
leftValue = ParseValue(part);
return 1;
}
static int ParseCommaDelimitedValues(ReadOnlySpan<char> part, in Span<UnitValue> outValues)
{
int valueIndex = 0;
while (true)
{
if (valueIndex >= outValues.Length)
{
throw new FormatException("Too many provided values.");
}
var commaIndex = part.IndexOf(',');
if (commaIndex == -1)
{
if (!part.IsWhiteSpace())
{
outValues[valueIndex++] = ParseValue(part);
}
break;
}
var valuePart = part.Slice(0, commaIndex).Trim();
outValues[valueIndex++] = ParseValue(valuePart);
part = part.Slice(commaIndex + 1, part.Length - commaIndex - 1);
}
return valueIndex;
}
switch (function)
{
case TransformFunction.Scale:
case TransformFunction.ScaleX:
case TransformFunction.ScaleY:
{
var scaleX = UnitValue.One;
var scaleY = UnitValue.One;
int count = ParseValuePair(functionPart, ref scaleX, ref scaleY);
if (count != 1 && (function == TransformFunction.ScaleX || function == TransformFunction.ScaleY))
{
ThrowFormatInvalidValueCount(function, 1);
}
VerifyZeroOrUnit(function, in scaleX, Unit.None);
VerifyZeroOrUnit(function, in scaleY, Unit.None);
if (function == TransformFunction.ScaleY)
{
scaleY = scaleX;
scaleX = UnitValue.One;
}
else if (function == TransformFunction.Scale && count == 1)
{
scaleY = scaleX;
}
builder.AppendScale(scaleX.Value, scaleY.Value);
break;
}
case TransformFunction.Skew:
case TransformFunction.SkewX:
case TransformFunction.SkewY:
{
var skewX = UnitValue.Zero;
var skewY = UnitValue.Zero;
int count = ParseValuePair(functionPart, ref skewX, ref skewY);
if (count != 1 && (function == TransformFunction.SkewX || function == TransformFunction.SkewY))
{
ThrowFormatInvalidValueCount(function, 1);
}
VerifyZeroOrAngle(function, in skewX);
VerifyZeroOrAngle(function, in skewY);
if (function == TransformFunction.SkewY)
{
skewY = skewX;
skewX = UnitValue.Zero;
}
builder.AppendSkew(ToRadians(in skewX), ToRadians(in skewY));
break;
}
case TransformFunction.Rotate:
{
var angle = UnitValue.Zero;
UnitValue _ = default;
int count = ParseValuePair(functionPart, ref angle, ref _);
if (count != 1)
{
ThrowFormatInvalidValueCount(function, 1);
}
VerifyZeroOrAngle(function, in angle);
builder.AppendRotate(ToRadians(in angle));
break;
}
case TransformFunction.Translate:
case TransformFunction.TranslateX:
case TransformFunction.TranslateY:
{
var translateX = UnitValue.Zero;
var translateY = UnitValue.Zero;
int count = ParseValuePair(functionPart, ref translateX, ref translateY);
if (count != 1 && (function == TransformFunction.TranslateX || function == TransformFunction.TranslateY))
{
ThrowFormatInvalidValueCount(function, 1);
}
VerifyZeroOrUnit(function, in translateX, Unit.Pixel);
VerifyZeroOrUnit(function, in translateY, Unit.Pixel);
if (function == TransformFunction.TranslateY)
{
translateY = translateX;
translateX = UnitValue.Zero;
}
builder.AppendTranslate(translateX.Value, translateY.Value);
break;
}
case TransformFunction.Matrix:
{
Span<UnitValue> values = stackalloc UnitValue[6];
int count = ParseCommaDelimitedValues(functionPart, in values);
if (count != 6)
{
ThrowFormatInvalidValueCount(function, 6);
}
foreach (UnitValue value in values)
{
VerifyZeroOrUnit(function, value, Unit.None);
}
var matrix = new Matrix(
values[0].Value,
values[1].Value,
values[2].Value,
values[3].Value,
values[4].Value,
values[5].Value);
builder.AppendMatrix(matrix);
break;
}
}
}
private static void VerifyZeroOrUnit(TransformFunction function, in UnitValue value, Unit unit)
{
bool isZero = value.Unit == Unit.None && value.Value == 0d;
if (!isZero && value.Unit != unit)
{
ThrowFormatInvalidValue(function, in value);
}
}
private static void VerifyZeroOrAngle(TransformFunction function, in UnitValue value)
{
if (value.Value != 0d && !IsAngleUnit(value.Unit))
{
ThrowFormatInvalidValue(function, in value);
}
}
private static bool IsAngleUnit(Unit unit)
{
switch (unit)
{
case Unit.Radian:
case Unit.Gradian:
case Unit.Degree:
case Unit.Turn:
{
return true;
}
}
return false;
}
private static void ThrowFormatInvalidValue(TransformFunction function, in UnitValue value)
{
var unitString = value.Unit == Unit.None ? string.Empty : value.Unit.ToString();
throw new FormatException($"Invalid value {value.Value} {unitString} for {function}");
}
private static void ThrowFormatInvalidValueCount(TransformFunction function, int count)
{
throw new FormatException($"Invalid format. {function} expects {count} value(s).");
}
private static Unit ParseUnit(in ReadOnlySpan<char> part)
{
foreach (var (name, unit) in s_unitMapping)
{
if (part.Equals(name.AsSpan(), StringComparison.OrdinalIgnoreCase))
{
return unit;
}
}
throw new FormatException($"Invalid unit: {part.ToString()}");
}
private static TransformFunction ParseTransformFunction(in ReadOnlySpan<char> part)
{
foreach (var (name, transformFunction) in s_functionMapping)
{
if (part.Equals(name.AsSpan(), StringComparison.OrdinalIgnoreCase))
{
return transformFunction;
}
}
return TransformFunction.Invalid;
}
private static double ToRadians(in UnitValue value)
{
return value.Unit switch
{
Unit.Radian => value.Value,
Unit.Gradian => MathUtilities.Grad2Rad(value.Value),
Unit.Degree => MathUtilities.Deg2Rad(value.Value),
Unit.Turn => MathUtilities.Turn2Rad(value.Value),
_ => value.Value
};
}
private enum Unit
{
None,
Pixel,
Radian,
Gradian,
Degree,
Turn
}
private readonly struct UnitValue
{
public readonly Unit Unit;
public readonly double Value;
public UnitValue(Unit unit, double value)
{
Unit = unit;
Value = value;
}
public static UnitValue Zero => new UnitValue(Unit.None, 0);
public static UnitValue One => new UnitValue(Unit.None, 1);
}
private enum TransformFunction
{
Invalid,
Translate,
TranslateX,
TranslateY,
Scale,
ScaleX,
ScaleY,
Skew,
SkewX,
SkewY,
Rotate,
Matrix
}
}
}

1
src/Avalonia.Visuals/Properties/AssemblyInfo.cs

@ -6,6 +6,7 @@ using Avalonia.Metadata;
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media.Imaging")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media.Transformation")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")]
[assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests")]

13
src/Avalonia.Visuals/Size.cs

@ -189,7 +189,7 @@ namespace Avalonia
}
/// <summary>
/// Returns a boolean indicating whether the size is equal to the other given size.
/// Returns a boolean indicating whether the size is equal to the other given size (bitwise).
/// </summary>
/// <param name="other">The other size to test equality against.</param>
/// <returns>True if this size is equal to other; False otherwise.</returns>
@ -201,6 +201,17 @@ namespace Avalonia
// ReSharper enable CompareOfFloatsByEqualityOperator
}
/// <summary>
/// Returns a boolean indicating whether the size is equal to the other given size (numerically).
/// </summary>
/// <param name="other">The other size to test equality against.</param>
/// <returns>True if this size is equal to other; False otherwise.</returns>
public bool NearlyEquals(Size other)
{
return MathUtilities.AreClose(_width, other._width) &&
MathUtilities.AreClose(_height, other._height);
}
/// <summary>
/// Checks for equality between a size and an object.
/// </summary>

52
src/Avalonia.Visuals/Vector.cs

@ -2,7 +2,8 @@ using System;
using System.Globalization;
using Avalonia.Animation.Animators;
using Avalonia.Utilities;
using JetBrains.Annotations;
#nullable enable
namespace Avalonia
{
@ -17,20 +18,20 @@ namespace Avalonia
}
/// <summary>
/// The X vector.
/// The X component.
/// </summary>
private readonly double _x;
/// <summary>
/// The Y vector.
/// The Y component.
/// </summary>
private readonly double _y;
/// <summary>
/// Initializes a new instance of the <see cref="Vector"/> structure.
/// </summary>
/// <param name="x">The X vector.</param>
/// <param name="y">The Y vector.</param>
/// <param name="x">The X component.</param>
/// <param name="y">The Y component.</param>
public Vector(double x, double y)
{
_x = x;
@ -38,12 +39,12 @@ namespace Avalonia
}
/// <summary>
/// Gets the X vector.
/// Gets the X component.
/// </summary>
public double X => _x;
/// <summary>
/// Gets the Y vector.
/// Gets the Y component.
/// </summary>
public double Y => _y;
@ -57,18 +58,18 @@ namespace Avalonia
}
/// <summary>
/// Calculates the dot product of two vectors
/// Calculates the dot product of two vectors.
/// </summary>
/// <param name="a">First vector</param>
/// <param name="b">Second vector</param>
/// <returns>The dot product</returns>
/// <param name="a">First vector.</param>
/// <param name="b">Second vector.</param>
/// <returns>The dot product.</returns>
public static double operator *(Vector a, Vector b)
=> Dot(a, b);
/// <summary>
/// Scales a vector.
/// </summary>
/// <param name="vector">The vector</param>
/// <param name="vector">The vector.</param>
/// <param name="scale">The scaling factor.</param>
/// <returns>The scaled vector.</returns>
public static Vector operator *(Vector vector, double scale)
@ -77,7 +78,7 @@ namespace Avalonia
/// <summary>
/// Scales a vector.
/// </summary>
/// <param name="vector">The vector</param>
/// <param name="vector">The vector.</param>
/// <param name="scale">The divisor.</param>
/// <returns>The scaled vector.</returns>
public static Vector operator /(Vector vector, double scale)
@ -100,12 +101,12 @@ namespace Avalonia
}
/// <summary>
/// Length of the vector
/// Length of the vector.
/// </summary>
public double Length => Math.Sqrt(SquaredLength);
/// <summary>
/// Squared Length of the vector
/// Squared Length of the vector.
/// </summary>
public double SquaredLength => _x * _x + _y * _y;
@ -154,9 +155,8 @@ namespace Avalonia
/// <returns>True if vectors are nearly equal.</returns>
public bool NearlyEquals(Vector other)
{
const float tolerance = float.Epsilon;
return Math.Abs(_x - other._x) < tolerance && Math.Abs(_y - other._y) < tolerance;
return MathUtilities.AreClose(_x, other._x) &&
MathUtilities.AreClose(_y, other._y);
}
public override bool Equals(object obj) => obj is Vector other && Equals(other);
@ -189,9 +189,9 @@ namespace Avalonia
}
/// <summary>
/// Returns a new vector with the specified X coordinate.
/// Returns a new vector with the specified X component.
/// </summary>
/// <param name="x">The X coordinate.</param>
/// <param name="x">The X component.</param>
/// <returns>The new vector.</returns>
public Vector WithX(double x)
{
@ -199,9 +199,9 @@ namespace Avalonia
}
/// <summary>
/// Returns a new vector with the specified Y coordinate.
/// Returns a new vector with the specified Y component.
/// </summary>
/// <param name="y">The Y coordinate.</param>
/// <param name="y">The Y component.</param>
/// <returns>The new vector.</returns>
public Vector WithY(double y)
{
@ -311,25 +311,25 @@ namespace Avalonia
=> new Vector(-vector._x, -vector._y);
/// <summary>
/// Returnes the vector (0.0, 0.0)
/// Returns the vector (0.0, 0.0).
/// </summary>
public static Vector Zero
=> new Vector(0, 0);
/// <summary>
/// Returnes the vector (1.0, 1.0)
/// Returns the vector (1.0, 1.0).
/// </summary>
public static Vector One
=> new Vector(1, 1);
/// <summary>
/// Returnes the vector (1.0, 0.0)
/// Returns the vector (1.0, 0.0).
/// </summary>
public static Vector UnitX
=> new Vector(1, 0);
/// <summary>
/// Returnes the vector (0.0, 1.0)
/// Returns the vector (0.0, 1.0).
/// </summary>
public static Vector UnitY
=> new Vector(0, 1);

14
src/Avalonia.Visuals/Visual.cs

@ -68,8 +68,8 @@ namespace Avalonia
/// <summary>
/// Defines the <see cref="RenderTransform"/> property.
/// </summary>
public static readonly StyledProperty<Transform> RenderTransformProperty =
AvaloniaProperty.Register<Visual, Transform>(nameof(RenderTransform));
public static readonly StyledProperty<ITransform> RenderTransformProperty =
AvaloniaProperty.Register<Visual, ITransform>(nameof(RenderTransform));
/// <summary>
/// Defines the <see cref="RenderTransformOrigin"/> property.
@ -219,7 +219,7 @@ namespace Avalonia
/// <summary>
/// Gets the render transform of the control.
/// </summary>
public Transform RenderTransform
public ITransform RenderTransform
{
get { return GetValue(RenderTransformProperty); }
set { SetValue(RenderTransformProperty, value); }
@ -391,9 +391,9 @@ namespace Avalonia
_visualRoot = e.Root;
if (RenderTransform != null)
if (RenderTransform is IMutableTransform mutableTransform)
{
RenderTransform.Changed += RenderTransformChanged;
mutableTransform.Changed += RenderTransformChanged;
}
EnableTransitions();
@ -428,9 +428,9 @@ namespace Avalonia
_visualRoot = null;
if (RenderTransform != null)
if (RenderTransform is IMutableTransform mutableTransform)
{
RenderTransform.Changed -= RenderTransformChanged;
mutableTransform.Changed -= RenderTransformChanged;
}
DisableTransitions();

2
src/Avalonia.Visuals/VisualTree/IVisual.cs

@ -76,7 +76,7 @@ namespace Avalonia.VisualTree
/// <summary>
/// Gets or sets the render transform of the control.
/// </summary>
Transform RenderTransform { get; set; }
ITransform RenderTransform { get; set; }
/// <summary>
/// Gets or sets the render transform origin of the control.

2
src/Avalonia.X11/X11Window.cs

@ -922,7 +922,7 @@ namespace Avalonia.X11
public IScreenImpl Screen => _platform.Screens;
public Size MaxClientSize => _platform.X11Screens.Screens.Select(s => s.Bounds.Size.ToSize(s.PixelDensity))
public Size MaxAutoSizeHint => _platform.X11Screens.Screens.Select(s => s.Bounds.Size.ToSize(s.PixelDensity))
.OrderByDescending(x => x.Width + x.Height).FirstOrDefault();

18
src/Avalonia.X11/XI2Manager.cs

@ -97,7 +97,7 @@ namespace Avalonia.X11
{
_platform = platform;
_x11 = platform.Info;
_multitouch = platform.Options?.EnableMultiTouch ?? false;
_multitouch = platform.Options?.EnableMultiTouch ?? true;
var devices =(XIDeviceInfo*) XIQueryDevice(_x11.Display,
(int)XiPredefinedDeviceId.XIAllMasterDevices, out int num);
for (var c = 0; c < num; c++)
@ -237,6 +237,22 @@ namespace Avalonia.X11
RawPointerEventType.Move, ev.Position, ev.Modifiers));
}
if (ev.Type == XiEventType.XI_ButtonPress && ev.Button >= 4 && ev.Button <= 7 && !ev.Emulated)
{
Vector? scrollDelta = ev.Button switch
{
4 => new Vector(0, 1),
5 => new Vector(0, -1),
6 => new Vector(1, 0),
7 => new Vector(-1, 0),
_ => null
};
if (scrollDelta.HasValue)
client.ScheduleXI2Input(new RawMouseWheelEventArgs(client.MouseDevice, ev.Timestamp,
client.InputRoot, ev.Position, scrollDelta.Value, ev.Modifiers));
}
if (ev.Type == XiEventType.XI_ButtonPress || ev.Type == XiEventType.XI_ButtonRelease)
{
var down = ev.Type == XiEventType.XI_ButtonPress;

26
src/Windows/Avalonia.Win32/PopupImpl.cs

@ -8,12 +8,35 @@ namespace Avalonia.Win32
class PopupImpl : WindowImpl, IPopupImpl
{
private bool _dropShadowHint = true;
private Size? _maxAutoSize;
public override void Show()
{
UnmanagedMethods.ShowWindow(Handle.Handle, UnmanagedMethods.ShowWindowCommand.ShowNoActivate);
}
public override Size MaxAutoSizeHint
{
get
{
if (_maxAutoSize is null)
{
var monitor = UnmanagedMethods.MonitorFromWindow(
Hwnd,
UnmanagedMethods.MONITOR.MONITOR_DEFAULTTONEAREST);
if (monitor != IntPtr.Zero)
{
var info = UnmanagedMethods.MONITORINFO.Create();
UnmanagedMethods.GetMonitorInfo(monitor, ref info);
_maxAutoSize = info.rcWork.ToPixelRect().ToRect(Scaling).Size;
}
}
return _maxAutoSize ?? Size.Infinity;
}
}
protected override IntPtr CreateWindowOverride(ushort atom)
{
UnmanagedMethods.WindowStyles style =
@ -47,6 +70,9 @@ namespace Avalonia.Win32
{
switch ((UnmanagedMethods.WindowsMessage)msg)
{
case UnmanagedMethods.WindowsMessage.WM_DISPLAYCHANGE:
_maxAutoSize = null;
goto default;
case UnmanagedMethods.WindowsMessage.WM_MOUSEACTIVATE:
return (IntPtr)UnmanagedMethods.MouseActivate.MA_NOACTIVATE;
default:

2
src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs

@ -393,6 +393,8 @@ namespace Avalonia.Win32
case WindowsMessage.WM_GETMINMAXINFO:
{
MINMAXINFO mmi = Marshal.PtrToStructure<MINMAXINFO>(lParam);
_maxTrackSize = mmi.ptMaxTrackSize;
if (_minSize.Width > 0)
{

16
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -66,6 +66,7 @@ namespace Avalonia.Win32
private OleDropTarget _dropTarget;
private Size _minSize;
private Size _maxSize;
private POINT _maxTrackSize;
private WindowImpl _parent;
public WindowImpl()
@ -168,16 +169,7 @@ namespace Avalonia.Win32
public IPlatformHandle Handle { get; private set; }
public Size MaxClientSize
{
get
{
return (new Size(
GetSystemMetrics(SystemMetric.SM_CXMAXTRACK),
GetSystemMetrics(SystemMetric.SM_CYMAXTRACK))
- BorderThickness) / Scaling;
}
}
public virtual Size MaxAutoSizeHint => new Size(_maxTrackSize.X / Scaling, _maxTrackSize.Y / Scaling);
public IMouseDevice MouseDevice => _mouseDevice;
@ -211,6 +203,8 @@ namespace Avalonia.Win32
public WindowTransparencyLevel TransparencyLevel { get; private set; }
protected IntPtr Hwnd => _hwnd;
public void SetTransparencyLevelHint (WindowTransparencyLevel transparencyLevel)
{
TransparencyLevel = EnableBlur(transparencyLevel);
@ -577,7 +571,7 @@ namespace Avalonia.Win32
Handle = new PlatformHandle(_hwnd, PlatformConstants.WindowHandleType);
_multitouch = Win32Platform.Options.EnableMultitouch ?? false;
_multitouch = Win32Platform.Options.EnableMultitouch ?? true;
if (_multitouch)
{

10
tests/Avalonia.Animation.UnitTests/TestClock.cs

@ -5,10 +5,12 @@ namespace Avalonia.Animation.UnitTests
{
internal class TestClock : IClock, IDisposable
{
private TimeSpan _curTime;
private IObserver<TimeSpan> _observer;
public PlayState PlayState { get; set; } = PlayState.Run;
public void Dispose()
{
_observer?.OnCompleted();
@ -19,6 +21,12 @@ namespace Avalonia.Animation.UnitTests
_observer?.OnNext(time);
}
public void Pulse(TimeSpan time)
{
_curTime += time;
_observer?.OnNext(_curTime);
}
public IDisposable Subscribe(IObserver<TimeSpan> observer)
{
_observer = observer;

88
tests/Avalonia.Animation.UnitTests/TransitionsTests.cs

@ -10,13 +10,11 @@ namespace Avalonia.Animation.UnitTests
[Fact]
public void Check_Transitions_Interpolation_Negative_Bounds_Clamp()
{
var clock = new MockGlobalClock();
var clock = new TestClock();
using (UnitTestApplication.Start(new TestServices(globalClock: clock)))
var border = new Border
{
var border = new Border
{
Transitions = new Transitions
Transitions = new Transitions
{
new DoubleTransition
{
@ -24,27 +22,25 @@ namespace Avalonia.Animation.UnitTests
Property = Border.OpacityProperty,
}
}
};
};
border.Opacity = 0;
border.Opacity = 0;
clock.Pulse(TimeSpan.FromSeconds(0));
clock.Pulse(TimeSpan.FromSeconds(-0.5));
clock.Pulse(TimeSpan.FromSeconds(0));
clock.Pulse(TimeSpan.FromSeconds(-0.5));
Assert.Equal(0, border.Opacity);
Assert.Equal(0, border.Opacity);
}
}
[Fact]
public void Check_Transitions_Interpolation_Positive_Bounds_Clamp()
{
var clock = new MockGlobalClock();
var clock = new TestClock();
using (UnitTestApplication.Start(new TestServices(globalClock: clock)))
var border = new Border
{
var border = new Border
{
Transitions = new Transitions
Transitions = new Transitions
{
new DoubleTransition
{
@ -52,34 +48,62 @@ namespace Avalonia.Animation.UnitTests
Property = Border.OpacityProperty,
}
}
};
};
border.Opacity = 0;
border.Opacity = 0;
clock.Pulse(TimeSpan.FromSeconds(0));
clock.Pulse(TimeSpan.FromMilliseconds(1001));
clock.Pulse(TimeSpan.FromSeconds(0));
clock.Pulse(TimeSpan.FromMilliseconds(1001));
Assert.Equal(0, border.Opacity);
Assert.Equal(0, border.Opacity);
}
}
[Fact]
public void TransitionInstance_With_Zero_Duration_Is_Completed_On_First_Tick()
{
var clock = new MockGlobalClock();
var clock = new TestClock();
using (UnitTestApplication.Start(new TestServices(globalClock: clock)))
int i = 0;
var inst = new TransitionInstance(clock, TimeSpan.Zero, TimeSpan.Zero).Subscribe(nextValue =>
{
int i = 0;
var inst = new TransitionInstance(clock, TimeSpan.Zero).Subscribe(nextValue =>
switch (i++)
{
switch (i++)
{
case 0: Assert.Equal(0, nextValue); break;
case 1: Assert.Equal(1d, nextValue); break;
}
});
case 0: Assert.Equal(0, nextValue); break;
case 1: Assert.Equal(1d, nextValue); break;
}
});
clock.Pulse(TimeSpan.FromMilliseconds(10));
}
[Fact]
public void TransitionInstance_Properly_Calculates_Delay_And_Duration_Values()
{
var clock = new TestClock();
int i = -1;
var inst = new TransitionInstance(clock, TimeSpan.FromMilliseconds(30), TimeSpan.FromMilliseconds(70)).Subscribe(nextValue =>
{
switch (i++)
{
case 0: Assert.Equal(0, nextValue); break;
case 1: Assert.Equal(0, nextValue); break;
case 2: Assert.Equal(0, nextValue); break;
case 3: Assert.Equal(0, nextValue); break;
case 4: Assert.Equal(Math.Round(10d / 70d, 4), Math.Round(nextValue, 4)); break;
case 5: Assert.Equal(Math.Round(20d / 70d, 4), Math.Round(nextValue, 4)); break;
case 6: Assert.Equal(Math.Round(30d / 70d, 4), Math.Round(nextValue, 4)); break;
case 7: Assert.Equal(Math.Round(40d / 70d, 4), Math.Round(nextValue, 4)); break;
case 8: Assert.Equal(Math.Round(50d / 70d, 4), Math.Round(nextValue, 4)); break;
case 9: Assert.Equal(Math.Round(60d / 70d, 4), Math.Round(nextValue, 4)); break;
case 10: Assert.Equal(1d, nextValue); break;
}
});
for (int z = 0; z <= 10; z++)
{
clock.Pulse(TimeSpan.FromMilliseconds(10));
}
}

109
tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs

@ -92,14 +92,14 @@ namespace Avalonia.Base.UnitTests.Utilities
}
[Fact]
public void Clamp_Input_NaN_Return_NaN()
public void Float_Clamp_Input_NaN_Return_NaN()
{
var clamp = MathUtilities.Clamp(double.NaN, 0.0, 1.0);
Assert.True(double.IsNaN(clamp));
}
[Fact]
public void Clamp_Input_NegativeInfinity_Return_Min()
public void Float_Clamp_Input_NegativeInfinity_Return_Min()
{
const double min = 0.0;
const double max = 1.0;
@ -108,12 +108,115 @@ namespace Avalonia.Base.UnitTests.Utilities
}
[Fact]
public void Clamp_Input_PositiveInfinity_Return_Max()
public void Float_Clamp_Input_PositiveInfinity_Return_Max()
{
const double min = 0.0;
const double max = 1.0;
var actual = MathUtilities.Clamp(double.PositiveInfinity, min, max);
Assert.Equal(max, actual);
}
[Fact]
public void Double_Float_Zero_Less_Than_One()
{
var actual = MathUtilities.LessThan(0d, 1d);
Assert.True(actual);
}
[Fact]
public void Single_Float_Zero_Less_Than_One()
{
var actual = MathUtilities.LessThan(0f, 1f);
Assert.True(actual);
}
[Fact]
public void Double_Float_One_Not_Less_Than_Zero()
{
var actual = MathUtilities.LessThan(1d, 0d);
Assert.False(actual);
}
[Fact]
public void Single_Float_One_Not_Less_Than_Zero()
{
var actual = MathUtilities.LessThan(1f, 0f);
Assert.False(actual);
}
[Fact]
public void Double_Float_Zero_Not_Greater_Than_One()
{
var actual = MathUtilities.GreaterThan(0d, 1d);
Assert.False(actual);
}
[Fact]
public void Single_Float_Zero_Not_Greater_Than_One()
{
var actual = MathUtilities.GreaterThan(0f, 1f);
Assert.False(actual);
}
[Fact]
public void Double_Float_One_Greater_Than_Zero()
{
var actual = MathUtilities.GreaterThan(1d, 0d);
Assert.True(actual);
}
[Fact]
public void Single_Float_One_Greater_Than_Zero()
{
var actual = MathUtilities.GreaterThan(1f, 0f);
Assert.True(actual);
}
[Fact]
public void Double_Float_One_Less_Than_Or_Close_One()
{
var actual = MathUtilities.LessThanOrClose(1d, 1d);
Assert.True(actual);
}
[Fact]
public void Single_Float_One_Less_Than_Or_Close_One()
{
var actual = MathUtilities.LessThanOrClose(1f, 1f);
Assert.True(actual);
}
[Fact]
public void Double_Float_One_Greater_Than_Or_Close_One()
{
var actual = MathUtilities.GreaterThanOrClose(1d, 1d);
Assert.True(actual);
}
[Fact]
public void Single_Float_One_Greater_Than_Or_Close_One()
{
var actual = MathUtilities.GreaterThanOrClose(1f, 1f);
Assert.True(actual);
}
[Fact]
public void Round_Layout_Value_Without_DPI_Aware()
{
const double value = 42.5;
var expectedValue = Math.Round(value);
var actualValue = MathUtilities.RoundLayoutValue(value, 1.0);
Assert.Equal(expectedValue, actualValue);
}
[Fact]
public void Round_Layout_Value_With_DPI_Aware()
{
const double dpiScale = 1.25;
const double value = 42.5;
var expectedValue = Math.Round(value * dpiScale) / dpiScale;
var actualValue = MathUtilities.RoundLayoutValue(value, dpiScale);
Assert.Equal(expectedValue, actualValue);
}
}
}

16
tests/Avalonia.Benchmarks/Visuals/MatrixBenchmarks.cs

@ -0,0 +1,16 @@
using BenchmarkDotNet.Attributes;
namespace Avalonia.Benchmarks.Visuals
{
[MemoryDiagnoser, InProcess]
public class MatrixBenchmarks
{
private static readonly Matrix s_data = Matrix.Identity;
[Benchmark(Baseline = true)]
public bool Decompose()
{
return Matrix.TryDecomposeTransform(s_data, out Matrix.Decomposed decomposed);
}
}
}

20
tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
@ -181,18 +182,21 @@ namespace Avalonia.Controls.UnitTests.Primitives
}
[Fact]
public void Child_Should_Be_Measured_With_Infinity()
public void Child_Should_Be_Measured_With_MaxAutoSizeHint()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var child = new ChildControl();
var window = new Window();
var target = CreateTarget(window);
var popupImpl = MockWindowingPlatform.CreatePopupMock(window.PlatformImpl);
popupImpl.Setup(x => x.MaxAutoSizeHint).Returns(new Size(1200, 1000));
var target = CreateTarget(window, popupImpl.Object);
target.Content = child;
target.Show();
Assert.Equal(Size.Infinity, child.MeasureSize);
Assert.Equal(1, child.MeasureSizes.Count);
Assert.Equal(new Size(1200, 1000), child.MeasureSizes[0]);
}
}
@ -210,7 +214,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.Content = child;
target.Show();
Assert.Equal(new Size(500, 600), child.MeasureSize);
Assert.Equal(1, child.MeasureSizes.Count);
Assert.Equal(new Size(500, 600), child.MeasureSizes[0]);
}
}
@ -228,7 +233,8 @@ namespace Avalonia.Controls.UnitTests.Primitives
target.Content = child;
target.Show();
Assert.Equal(new Size(500, 600), child.MeasureSize);
Assert.Equal(1, child.MeasureSizes.Count);
Assert.Equal(new Size(500, 600), child.MeasureSizes[0]);
}
}
@ -365,11 +371,11 @@ namespace Avalonia.Controls.UnitTests.Primitives
private class ChildControl : Control
{
public Size MeasureSize { get; private set; }
public List<Size> MeasureSizes { get; } = new List<Size>();
protected override Size MeasureOverride(Size availableSize)
{
MeasureSize = availableSize;
MeasureSizes.Add(availableSize);
return base.MeasureOverride(availableSize);
}
}

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

@ -297,12 +297,12 @@ namespace Avalonia.Controls.UnitTests
{
var parentWindowImpl = MockWindowingPlatform.CreateWindowMock();
parentWindowImpl.Setup(x => x.ClientSize).Returns(new Size(800, 480));
parentWindowImpl.Setup(x => x.MaxClientSize).Returns(new Size(1920, 1080));
parentWindowImpl.Setup(x => x.MaxAutoSizeHint).Returns(new Size(1920, 1080));
parentWindowImpl.Setup(x => x.Scaling).Returns(1);
var windowImpl = MockWindowingPlatform.CreateWindowMock();
windowImpl.Setup(x => x.ClientSize).Returns(new Size(320, 200));
windowImpl.Setup(x => x.MaxClientSize).Returns(new Size(1920, 1080));
windowImpl.Setup(x => x.MaxAutoSizeHint).Returns(new Size(1920, 1080));
windowImpl.Setup(x => x.Scaling).Returns(1);
var parentWindowServices = TestServices.StyledWindow.With(
@ -381,12 +381,15 @@ namespace Avalonia.Controls.UnitTests
}
[Fact]
public void Child_Should_Be_Measured_With_Infinity_If_SizeToContent_Is_WidthAndHeight()
public void Child_Should_Be_Measured_With_MaxAutoSizeHint_If_SizeToContent_Is_WidthAndHeight()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var windowImpl = MockWindowingPlatform.CreateWindowMock();
windowImpl.Setup(x => x.MaxAutoSizeHint).Returns(new Size(1200, 1000));
var child = new ChildControl();
var target = new Window
var target = new Window(windowImpl.Object)
{
Width = 100,
Height = 50,
@ -394,10 +397,10 @@ namespace Avalonia.Controls.UnitTests
Content = child
};
Show(target);
target.Show();
Assert.Equal(1, child.MeasureSizes.Count);
Assert.Equal(Size.Infinity, child.MeasureSizes[0]);
Assert.Equal(new Size(1200, 1000), child.MeasureSizes[0]);
}
}

2
tests/Avalonia.UnitTests/MockWindowingPlatform.cs

@ -28,6 +28,7 @@ namespace Avalonia.UnitTests
windowImpl.SetupAllProperties();
windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize);
windowImpl.Setup(x => x.MaxAutoSizeHint).Returns(s_screenSize);
windowImpl.Setup(x => x.Scaling).Returns(1);
windowImpl.Setup(x => x.Screen).Returns(CreateScreenMock().Object);
windowImpl.Setup(x => x.Position).Returns(() => position);
@ -79,6 +80,7 @@ namespace Avalonia.UnitTests
popupImpl.SetupAllProperties();
popupImpl.Setup(x => x.ClientSize).Returns(() => clientSize);
popupImpl.Setup(x => x.MaxAutoSizeHint).Returns(s_screenSize);
popupImpl.Setup(x => x.Scaling).Returns(1);
popupImpl.Setup(x => x.PopupPositioner).Returns(positioner);

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

Loading…
Cancel
Save