diff --git a/readme.md b/readme.md
index 8ae3f1ad66..9d317cdd06 100644
--- a/readme.md
+++ b/readme.md
@@ -1,24 +1,20 @@
-
+[](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) [](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) [](#backers) [](#sponsors) 
+
+[](https://www.nuget.org/packages/Avalonia) [](https://www.nuget.org/packages/Avalonia) [](https://www.myget.org/gallery/avalonia-ci) 
-# Avalonia
+
-| Gitter Chat | Build Status (Win, Linux, OSX) | Open Collective | NuGet | MyGet |
-|---|---|---|---|---|
-| [](https://gitter.im/AvaloniaUI/Avalonia?utm_campaign=pr-badge&utm_content=badge&utm_medium=badge&utm_source=badge) | [](https://dev.azure.com/AvaloniaUI/AvaloniaUI/_build/latest?definitionId=4) | [](#backers) [](#sponsors) | [](https://www.nuget.org/packages/Avalonia) | [](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.
+
-**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
+
+
+
+
+
+
## 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.
diff --git a/samples/ControlCatalog/Pages/ToolTipPage.xaml b/samples/ControlCatalog/Pages/ToolTipPage.xaml
index cbe1e3059c..73d83e08f1 100644
--- a/samples/ControlCatalog/Pages/ToolTipPage.xaml
+++ b/samples/ControlCatalog/Pages/ToolTipPage.xaml
@@ -18,7 +18,7 @@
ToolTip.Tip="This is a ToolTip">
Hover Here
-
+
+
+
diff --git a/samples/RenderDemo/Pages/TransitionsPage.xaml b/samples/RenderDemo/Pages/TransitionsPage.xaml
new file mode 100644
index 0000000000..df7130a925
--- /dev/null
+++ b/samples/RenderDemo/Pages/TransitionsPage.xaml
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Hover to activate Transform Keyframe Animations.
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/RenderDemo/Pages/TransitionsPage.xaml.cs b/samples/RenderDemo/Pages/TransitionsPage.xaml.cs
new file mode 100644
index 0000000000..5f446c9e99
--- /dev/null
+++ b/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;
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Base/Properties/AssemblyInfo.cs b/src/Avalonia.Base/Properties/AssemblyInfo.cs
index 0664f22dcb..692982cdc6 100644
--- a/src/Avalonia.Base/Properties/AssemblyInfo.cs
+++ b/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")]
diff --git a/src/Avalonia.Base/Utilities/MathUtilities.cs b/src/Avalonia.Base/Utilities/MathUtilities.cs
index 7949a62949..06a1cd4ae5 100644
--- a/src/Avalonia.Base/Utilities/MathUtilities.cs
+++ b/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
/// The clamped value.
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;
}
}
+
+ ///
+ /// Converts an angle in degrees to radians.
+ ///
+ /// The angle in degrees.
+ /// The angle in radians.
+ public static double Deg2Rad(double angle)
+ {
+ return angle * (Math.PI / 180d);
+ }
+
+ ///
+ /// Converts an angle in gradians to radians.
+ ///
+ /// The angle in gradians.
+ /// The angle in radians.
+ public static double Grad2Rad(double angle)
+ {
+ return angle * (Math.PI / 200d);
+ }
+
+ ///
+ /// Converts an angle in turns to radians.
+ ///
+ /// The angle in turns.
+ /// The angle in radians.
+ 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}.");
+ }
}
}
diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs
index cfe47a09d5..7e893d131e 100644
--- a/src/Avalonia.Controls.DataGrid/DataGrid.cs
+++ b/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;
}
diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
index df2b03798a..e6cc7e5e40 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
+++ b/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;
}
diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumns.cs b/src/Avalonia.Controls.DataGrid/DataGridColumns.cs
index 16b63ad696..5b75bc73f9 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridColumns.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridColumns.cs
@@ -44,7 +44,7 @@ namespace Avalonia.Controls
/// The remaining amount of adjustment.
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
/// The remaining amount of adjustment.
private double AdjustStarColumnWidths(int displayIndex, double remainingAdjustment, bool userInitiated, Func 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
/// The remaining amount of adjustment.
private double DecreaseNonStarColumnWidths(int displayIndex, Func 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
/// The remaining amount of adjustment.
private double IncreaseNonStarColumnWidths(int displayIndex, Func 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;
}
diff --git a/src/Avalonia.Controls.DataGrid/DataGridLength.cs b/src/Avalonia.Controls.DataGrid/DataGridLength.cs
index 6a545a35ec..4841ddd494 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridLength.cs
+++ b/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);
diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs
index df200240ff..830eff1102 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs
+++ b/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;
diff --git a/src/Avalonia.Controls.DataGrid/DataGridRows.cs b/src/Avalonia.Controls.DataGrid/DataGridRows.cs
index fda14cf5b4..924156f5f4 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridRows.cs
+++ b/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
}
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs
index 0f513e7f42..6e0703c90f 100644
--- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs
+++ b/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)
}
}
}
diff --git a/src/Avalonia.Controls.DataGrid/Utils/DoubleUtil.cs b/src/Avalonia.Controls.DataGrid/Utils/DoubleUtil.cs
deleted file mode 100644
index ec0e8836d7..0000000000
--- a/src/Avalonia.Controls.DataGrid/Utils/DoubleUtil.cs
+++ /dev/null
@@ -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;
-
- ///
- /// 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*.
- ///
- ///
- /// bool - the result of the AreClose comparison.
- ///
- /// The first double to compare.
- /// The second double to compare.
- 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);
- }
-
- ///
- /// 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*.
- ///
- ///
- /// bool - the result of the GreaterThan comparison.
- ///
- /// The first double to compare.
- /// The second double to compare.
- public static bool GreaterThan(double value1, double value2)
- {
- return (value1 > value2) && !AreClose(value1, value2);
- }
-
- ///
- /// 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*.
- ///
- ///
- /// bool - the result of the GreaterThanOrClose comparison.
- ///
- /// The first double to compare.
- /// The second double to compare.
- public static bool GreaterThanOrClose(double value1, double value2)
- {
- return (value1 > value2) || AreClose(value1, value2);
- }
-
- ///
- /// IsZero - Returns whether or not the double is "close" to 0. Same as AreClose(double, 0),
- /// but this is faster.
- ///
- ///
- /// bool - the result of the IsZero comparison.
- ///
- /// The double to compare to 0.
- public static bool IsZero(double value)
- {
- return Math.Abs(value) < 10.0 * DBL_EPSILON;
- }
-
- ///
- /// 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*.
- ///
- ///
- /// bool - the result of the LessThan comparison.
- ///
- /// The first double to compare.
- /// The second double to compare.
- public static bool LessThan(double value1, double value2)
- {
- return (value1 < value2) && !AreClose(value1, value2);
- }
-
- ///
- /// 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*.
- ///
- ///
- /// bool - the result of the LessThanOrClose comparison.
- ///
- /// The first double to compare.
- /// The second double to compare.
- public static bool LessThanOrClose(double value1, double value2)
- {
- return (value1 < value2) || AreClose(value1, value2);
- }
- }
-}
diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs
index 86499530da..5929dd39d4 100644
--- a/src/Avalonia.Controls/ContextMenu.cs
+++ b/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
{
///
@@ -19,11 +20,59 @@ namespace Avalonia.Controls
///
public class ContextMenu : MenuBase, ISetterValue
{
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty HorizontalOffsetProperty =
+ Popup.HorizontalOffsetProperty.AddOwner();
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty VerticalOffsetProperty =
+ Popup.VerticalOffsetProperty.AddOwner();
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty PlacementAnchorProperty =
+ Popup.PlacementAnchorProperty.AddOwner();
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty PlacementConstraintAdjustmentProperty =
+ Popup.PlacementConstraintAdjustmentProperty.AddOwner();
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty PlacementGravityProperty =
+ Popup.PlacementGravityProperty.AddOwner();
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty PlacementModeProperty =
+ Popup.PlacementModeProperty.AddOwner();
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty PlacementRectProperty =
+ AvaloniaProperty.Register(nameof(PlacementRect));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty PlacementTargetProperty =
+ Popup.PlacementTargetProperty.AddOwner();
+
private static readonly ITemplate DefaultPanel =
new FuncTemplate(() => new StackPanel { Orientation = Orientation.Vertical });
- private Popup _popup;
- private List _attachedControls;
- private IInputElement _previousFocus;
+ private Popup? _popup;
+ private List? _attachedControls;
+ private IInputElement? _previousFocus;
///
/// Initializes a new instance of the class.
@@ -47,23 +96,107 @@ namespace Avalonia.Controls
///
static ContextMenu()
{
- ItemsPanelProperty.OverrideDefaultValue(typeof(ContextMenu), DefaultPanel);
+ ItemsPanelProperty.OverrideDefaultValue(DefaultPanel);
+ PlacementModeProperty.OverrideDefaultValue(PlacementMode.Pointer);
ContextMenuProperty.Changed.Subscribe(ContextMenuChanged);
}
+ ///
+ /// Gets or sets the Horizontal offset of the context menu in relation to the .
+ ///
+ public double HorizontalOffset
+ {
+ get { return GetValue(HorizontalOffsetProperty); }
+ set { SetValue(HorizontalOffsetProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the Vertical offset of the context menu in relation to the .
+ ///
+ public double VerticalOffset
+ {
+ get { return GetValue(VerticalOffsetProperty); }
+ set { SetValue(VerticalOffsetProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the anchor point on the when
+ /// is .
+ ///
+ public PopupAnchor PlacementAnchor
+ {
+ get { return GetValue(PlacementAnchorProperty); }
+ set { SetValue(PlacementAnchorProperty, value); }
+ }
+
+ ///
+ /// 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.
+ ///
+ public PopupPositionerConstraintAdjustment PlacementConstraintAdjustment
+ {
+ get { return GetValue(PlacementConstraintAdjustmentProperty); }
+ set { SetValue(PlacementConstraintAdjustmentProperty, value); }
+ }
+
+ ///
+ /// Gets or sets a value which defines in what direction the context menu should open
+ /// when is .
+ ///
+ public PopupGravity PlacementGravity
+ {
+ get { return GetValue(PlacementGravityProperty); }
+ set { SetValue(PlacementGravityProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the placement mode of the context menu in relation to the.
+ ///
+ public PlacementMode PlacementMode
+ {
+ get { return GetValue(PlacementModeProperty); }
+ set { SetValue(PlacementModeProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the the anchor rectangle within the parent that the context menu will be placed
+ /// relative to when is .
+ ///
+ ///
+ /// The placement rect defines a rectangle relative to around
+ /// which the popup will be opened, with determining which edge
+ /// of the placement target is used.
+ ///
+ /// If unset, the anchor rectangle will be the bounds of the .
+ ///
+ public Rect? PlacementRect
+ {
+ get { return GetValue(PlacementRectProperty); }
+ set { SetValue(PlacementRectProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the control that is used to determine the popup's position.
+ ///
+ public Control? PlacementTarget
+ {
+ get { return GetValue(PlacementTargetProperty); }
+ set { SetValue(PlacementTargetProperty, value); }
+ }
+
///
/// Occurs when the value of the
///
/// property is changing from false to true.
///
- public event CancelEventHandler ContextMenuOpening;
+ public event CancelEventHandler? ContextMenuOpening;
///
/// Occurs when the value of the
///
/// property is changing from true to false.
///
- public event CancelEventHandler ContextMenuClosing;
+ public event CancelEventHandler? ContextMenuClosing;
///
/// Called when the 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.
///
/// The control.
- 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.
diff --git a/src/Avalonia.Controls/LayoutTransformControl.cs b/src/Avalonia.Controls/LayoutTransformControl.cs
index 8d48f6646d..83ad2b3638 100644
--- a/src/Avalonia.Controls/LayoutTransformControl.cs
+++ b/src/Avalonia.Controls/LayoutTransformControl.cs
@@ -14,8 +14,8 @@ namespace Avalonia.Controls
///
public class LayoutTransformControl : Decorator
{
- public static readonly StyledProperty LayoutTransformProperty =
- AvaloniaProperty.Register(nameof(LayoutTransform));
+ public static readonly StyledProperty LayoutTransformProperty =
+ AvaloniaProperty.Register(nameof(LayoutTransform));
public static readonly StyledProperty UseRenderTransformProperty =
AvaloniaProperty.Register(nameof(LayoutTransform));
@@ -37,7 +37,7 @@ namespace Avalonia.Controls
///
/// Gets or sets a graphics transformation that should apply to this element when layout is performed.
///
- public Transform LayoutTransform
+ public ITransform LayoutTransform
{
get { return GetValue(LayoutTransformProperty); }
set { SetValue(LayoutTransformProperty, value); }
diff --git a/src/Avalonia.Controls/Primitives/IPopupHost.cs b/src/Avalonia.Controls/Primitives/IPopupHost.cs
index 74a3ca8818..e424bf683d 100644
--- a/src/Avalonia.Controls/Primitives/IPopupHost.cs
+++ b/src/Avalonia.Controls/Primitives/IPopupHost.cs
@@ -5,19 +5,70 @@ using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives
{
+ ///
+ /// Represents the top-level control opened by a .
+ ///
+ ///
+ /// A popup host can be either be a popup window created by the operating system
+ /// () or an which is created
+ /// on an .
+ ///
public interface IPopupHost : IDisposable
{
+ ///
+ /// Sets the control to display in the popup.
+ ///
+ ///
void SetChild(IControl control);
+
+ ///
+ /// Gets the presenter from the control's template.
+ ///
IContentPresenter Presenter { get; }
+
+ ///
+ /// Gets the root of the visual tree in the case where the popup is presented using a
+ /// separate visual tree.
+ ///
IVisual HostedVisualTreeRoot { get; }
+ ///
+ /// Raised when the control's template is applied.
+ ///
event EventHandler TemplateApplied;
+ ///
+ /// Configures the position of the popup according to a target control and a set of
+ /// placement parameters.
+ ///
+ /// The placement target.
+ /// The placement mode.
+ /// The offset, in device-independent pixels.
+ /// The anchor point.
+ /// The popup gravity.
+ ///
+ /// The anchor rect. If null, the bounds of will be used.
+ ///
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);
+
+ ///
+ /// Shows the popup.
+ ///
void Show();
+
+ ///
+ /// Hides the popup.
+ ///
void Hide();
+
+ ///
+ /// Binds the constraints of the popup host to a set of properties, usally those present on
+ /// .
+ ///
IDisposable BindConstraints(AvaloniaObject popup, StyledProperty widthProperty,
StyledProperty minWidthProperty, StyledProperty maxWidthProperty,
StyledProperty heightProperty, StyledProperty minHeightProperty,
diff --git a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
index 3dc9d302db..762d8d37a6 100644
--- a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
+++ b/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();
diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs
index 49315e1b25..ac4f805174 100644
--- a/src/Avalonia.Controls/Primitives/Popup.cs
+++ b/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);
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty PlacementAnchorProperty =
+ AvaloniaProperty.Register(nameof(PlacementAnchor));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty PlacementConstraintAdjustmentProperty =
+ AvaloniaProperty.Register(
+ nameof(PlacementConstraintAdjustment),
+ PopupPositionerConstraintAdjustment.FlipX | PopupPositionerConstraintAdjustment.FlipY |
+ PopupPositionerConstraintAdjustment.ResizeX | PopupPositionerConstraintAdjustment.ResizeY);
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty PlacementGravityProperty =
+ AvaloniaProperty.Register(nameof(PlacementGravity));
+
///
/// Defines the property.
///
public static readonly StyledProperty PlacementModeProperty =
AvaloniaProperty.Register(nameof(PlacementMode), defaultValue: PlacementMode.Bottom);
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty PlacementRectProperty =
+ AvaloniaProperty.Register(nameof(PlacementRect));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty PlacementTargetProperty =
+ AvaloniaProperty.Register(nameof(PlacementTarget));
+
#pragma warning disable 618
///
/// Defines the property.
@@ -63,12 +97,6 @@ namespace Avalonia.Controls.Primitives
public static readonly StyledProperty VerticalOffsetProperty =
AvaloniaProperty.Register(nameof(VerticalOffset));
- ///
- /// Defines the property.
- ///
- public static readonly StyledProperty PlacementTargetProperty =
- AvaloniaProperty.Register(nameof(PlacementTarget));
-
///
/// Defines the property.
///
@@ -145,6 +173,36 @@ namespace Avalonia.Controls.Primitives
set { SetAndRaise(IsOpenProperty, ref _isOpen, value); }
}
+ ///
+ /// Gets or sets the anchor point on the when
+ /// is .
+ ///
+ public PopupAnchor PlacementAnchor
+ {
+ get { return GetValue(PlacementAnchorProperty); }
+ set { SetValue(PlacementAnchorProperty, value); }
+ }
+
+ ///
+ /// 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.
+ ///
+ public PopupPositionerConstraintAdjustment PlacementConstraintAdjustment
+ {
+ get { return GetValue(PlacementConstraintAdjustmentProperty); }
+ set { SetValue(PlacementConstraintAdjustmentProperty, value); }
+ }
+
+ ///
+ /// Gets or sets a value which defines in what direction the popup should open
+ /// when is .
+ ///
+ public PopupGravity PlacementGravity
+ {
+ get { return GetValue(PlacementGravityProperty); }
+ set { SetValue(PlacementGravityProperty, value); }
+ }
+
///
/// Gets or sets the placement mode of the popup in relation to the .
///
@@ -154,6 +212,32 @@ namespace Avalonia.Controls.Primitives
set { SetValue(PlacementModeProperty, value); }
}
+ ///
+ /// Gets or sets the the anchor rectangle within the parent that the popup will be placed
+ /// relative to when is .
+ ///
+ ///
+ /// The placement rect defines a rectangle relative to around
+ /// which the popup will be opened, with determining which edge
+ /// of the placement target is used.
+ ///
+ /// If unset, the anchor rectangle will be the bounds of the .
+ ///
+ public Rect? PlacementRect
+ {
+ get { return GetValue(PlacementRectProperty); }
+ set { SetValue(PlacementRectProperty, value); }
+ }
+
+ ///
+ /// Gets or sets the control that is used to determine the popup's position.
+ ///
+ 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
}
///
- /// Gets or sets the Horizontal offset of the popup in relation to the
+ /// Gets or sets the Horizontal offset of the popup in relation to the .
///
public double HorizontalOffset
{
@@ -171,7 +255,7 @@ namespace Avalonia.Controls.Primitives
}
///
- /// Gets or sets the Vertical offset of the popup in relation to the
+ /// Gets or sets the Vertical offset of the popup in relation to the .
///
public double VerticalOffset
{
@@ -179,15 +263,6 @@ namespace Avalonia.Controls.Primitives
set { SetValue(VerticalOffsetProperty, value); }
}
- ///
- /// Gets or sets the control that is used to determine the popup's position.
- ///
- public Control? PlacementTarget
- {
- get { return GetValue(PlacementTargetProperty); }
- set { SetValue(PlacementTargetProperty, value); }
- }
-
///
/// 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>(popupHost, RootTemplateApplied,
(x, handler) => x.TemplateApplied += handler,
diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
index f0358ec04f..aed7dff0fe 100644
--- a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
+++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
@@ -50,46 +50,48 @@ using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives.PopupPositioning
{
///
- ///
- /// 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 .
///
+ ///
+ /// 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.
+ ///
public struct PopupPositionerParameters
{
- private PopupPositioningEdge _gravity;
- private PopupPositioningEdge _anchor;
+ private PopupGravity _gravity;
+ private PopupAnchor _anchor;
///
- /// 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.
///
public Size Size { get; set; }
///
- /// 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.
///
+ ///
+ /// The rectangle is relative to the parent geometry and may not extend outside the window
+ /// geometry of the popup's parent.
+ ///
public Rect AnchorRectangle { get; set; }
-
///
- /// 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.
///
- public PopupPositioningEdge Anchor
+ ///
+ /// 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.
+ ///
+ public PopupAnchor Anchor
{
get => _anchor;
set
@@ -100,66 +102,70 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
}
///
- /// 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.
///
- public PopupPositioningEdge Gravity
+ ///
+ /// 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.
+ ///
+ public PopupGravity Gravity
{
get => _gravity;
set
{
- PopupPositioningEdgeHelper.ValidateEdge(value);
+ PopupPositioningEdgeHelper.ValidateGravity(value);
_gravity = value;
}
}
///
- /// 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.
+ ///
+ ///
+ /// 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.
- ///
+ ///
public PopupPositionerConstraintAdjustment ConstraintAdjustment { get; set; }
-
+
///
/// 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.
///
+ ///
+ /// 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.
+ ///
public Point Offset { get; set; }
}
-
+
///
- /// 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.
///
+ ///
+ /// 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.
+ ///
[Flags]
public enum PopupPositionerConstraintAdjustment
{
@@ -171,79 +177,97 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
///
/// 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.
///
+ ///
+ /// 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.
+ ///
SlideX = 1,
-
///
- /// 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.
///
+ ///
+ /// 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.
+ ///
SlideY = 2,
///
- /// 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.
///
+ ///
+ /// 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.
+ /// ///
FlipX = 4,
///
- /// 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.
+ ///
+ ///
+ /// 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.
- ///
+ /// If the adjusted position also ends up being constrained, the resulting position of the
+ /// FlipY adjustment will be the one before the adjustment.
+ ///
FlipY = 8,
- All = SlideX|SlideY|FlipX|FlipY
+
+ ///
+ /// Horizontally resize the surface
+ ///
+ ///
+ /// Resize the surface horizontally so that it is completely unconstrained.
+ ///
+ ResizeX = 16,
+
+ ///
+ /// Vertically resize the surface
+ ///
+ ///
+ /// Resize the surface vertically so that it is completely unconstrained.
+ ///
+ 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);
+ }
}
+ ///
+ /// Defines the edges around an anchor rectangle on which a popup will open.
+ ///
[Flags]
- public enum PopupPositioningEdge
+ public enum PopupAnchor
{
+ ///
+ /// The center of the anchor rectangle.
+ ///
None,
+
+ ///
+ /// The top edge of the anchor rectangle.
+ ///
Top = 1,
+
+ ///
+ /// The bottom edge of the anchor rectangle.
+ ///
Bottom = 2,
+
+ ///
+ /// The left edge of the anchor rectangle.
+ ///
Left = 4,
+
+ ///
+ /// The right edge of the anchor rectangle.
+ ///
Right = 8,
+
+ ///
+ /// The top-left corner of the anchor rectangle.
+ ///
TopLeft = Top | Left,
+
+ ///
+ /// The top-right corner of the anchor rectangle.
+ ///
TopRight = Top | Right,
+
+ ///
+ /// The bottom-left corner of the anchor rectangle.
+ ///
BottomLeft = Bottom | Left,
+
+ ///
+ /// The bottom-right corner of the anchor rectangle.
+ ///
BottomRight = Bottom | Right,
-
+ ///
+ /// A mask for the vertical component flags.
+ ///
VerticalMask = Top | Bottom,
+
+ ///
+ /// A mask for the horizontal component flags.
+ ///
HorizontalMask = Left | Right,
+
+ ///
+ /// A mask for all flags.
+ ///
AllMask = VerticalMask|HorizontalMask
}
+ ///
+ /// Defines the direction in which a popup will open.
+ ///
+ [Flags]
+ public enum PopupGravity
+ {
+ ///
+ /// The popup will be centered over the anchor edge.
+ ///
+ None,
+
+ ///
+ /// The popup will be positioned above the anchor edge
+ ///
+ Top = 1,
+
+ ///
+ /// The popup will be positioned below the anchor edge
+ ///
+ Bottom = 2,
+
+ ///
+ /// The popup will be positioned to the left of the anchor edge
+ ///
+ Left = 4,
+
+ ///
+ /// The popup will be positioned to the right of the anchor edge
+ ///
+ Right = 8,
+
+ ///
+ /// The popup will be positioned to the top-left of the anchor edge
+ ///
+ TopLeft = Top | Left,
+
+ ///
+ /// The popup will be positioned to the top-right of the anchor edge
+ ///
+ TopRight = Top | Right,
+
+ ///
+ /// The popup will be positioned to the bottom-left of the anchor edge
+ ///
+ BottomLeft = Bottom | Left,
+
+ ///
+ /// The popup will be positioned to the bottom-right of the anchor edge
+ ///
+ BottomRight = Bottom | Right,
+ }
+
+ ///
+ /// Positions an .
+ ///
+ ///
+ /// 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 for platforms
+ /// on which popups can be arbitrarily positioned.
+ ///
public interface IPopupPositioner
{
+ ///
+ /// Updates the position of the associated according to the
+ /// specified parameters.
+ ///
+ /// The positioning parameters.
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)
{
diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs
index 07348cdf78..8c464c7aad 100644
--- a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositioner.cs
+++ b/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 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
}
}
+ ///
+ /// An implementation for platforms on which a popup can be
+ /// aritrarily positioned.
+ ///
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;
}
}
}
diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs
index 8e7e429a73..b0e3d1ab08 100644
--- a/src/Avalonia.Controls/Primitives/PopupPositioning/ManagedPopupPositionerPopupImplHelper.cs
+++ b/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;
}
}
diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs
index aab7a68795..854b0cf435 100644
--- a/src/Avalonia.Controls/Primitives/PopupRoot.cs
+++ b/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();
diff --git a/src/Avalonia.Controls/ToggleSwitch.cs b/src/Avalonia.Controls/ToggleSwitch.cs
new file mode 100644
index 0000000000..6c6426a31d
--- /dev/null
+++ b/src/Avalonia.Controls/ToggleSwitch.cs
@@ -0,0 +1,105 @@
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Templates;
+using Avalonia.LogicalTree;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// A Toggle Switch control.
+ ///
+ public class ToggleSwitch : ToggleButton
+ {
+ static ToggleSwitch()
+ {
+ OffContentProperty.Changed.AddClassHandler((x, e) => x.OffContentChanged(e));
+ OnContentProperty.Changed.AddClassHandler((x, e) => x.OnContentChanged(e));
+ }
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty