diff --git a/readme.md b/readme.md index 8ae3f1ad66..491b517e42 100644 --- a/readme.md +++ b/readme.md @@ -1,24 +1,20 @@ - +[![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) +
+[![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 + -| 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. + -**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. 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 - The clamped value. public static double Clamp(double val, double min, double max) { + if (min > max) + { + ThrowCannotBeGreaterThanException(min, max); + } + if (val < min) { return min; @@ -215,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. @@ -245,7 +250,7 @@ namespace Avalonia.Utilities { if (min > max) { - throw new ArgumentException($"{min} cannot be greater than {max}."); + ThrowCannotBeGreaterThanException(min, max); } if (val < min) @@ -291,5 +296,10 @@ namespace Avalonia.Utilities { 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/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 OffContentProperty = + AvaloniaProperty.Register(nameof(OffContent), defaultValue: "Off"); + + /// + /// Defines the property. + /// + public static readonly StyledProperty OffContentTemplateProperty = + AvaloniaProperty.Register(nameof(OffContentTemplate)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty OnContentProperty = + AvaloniaProperty.Register(nameof(OnContent), defaultValue: "On"); + + /// + /// Defines the property. + /// + public static readonly StyledProperty OnContentTemplateProperty = + AvaloniaProperty.Register(nameof(OnContentTemplate)); + + /// + /// Gets or Sets the Content that is displayed when in the On State. + /// + public object OnContent + { + get { return GetValue(OnContentProperty); } + set { SetValue(OnContentProperty, value); } + } + + /// + /// Gets or Sets the Content that is displayed when in the Off State. + /// + public object OffContent + { + get { return GetValue(OffContentProperty); } + set { SetValue(OffContentProperty, value); } + } + + /// + /// Gets or Sets the used to display the . + /// + public IDataTemplate OffContentTemplate + { + get { return GetValue(OffContentTemplateProperty); } + set { SetValue(OffContentTemplateProperty, value); } + } + + /// + /// Gets or Sets the used to display the . + /// + 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); + } + } + } +} + diff --git a/src/Avalonia.Native/OsxManagedPopupPositionerPopupImplHelper.cs b/src/Avalonia.Native/OsxManagedPopupPositionerPopupImplHelper.cs index e81c8853e8..8aa9b1a122 100644 --- a/src/Avalonia.Native/OsxManagedPopupPositionerPopupImplHelper.cs +++ b/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; } } diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index 83da5d3142..94d26e798b 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -52,4 +52,5 @@ + diff --git a/src/Avalonia.Themes.Default/ToggleSwitch.xaml b/src/Avalonia.Themes.Default/ToggleSwitch.xaml new file mode 100644 index 0000000000..ed172b52ab --- /dev/null +++ b/src/Avalonia.Themes.Default/ToggleSwitch.xaml @@ -0,0 +1,294 @@ + + + 0,0,0,6 + 6 + 6 + 154 + 20 + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml index 467f1f4ede..eb6cc610cc 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml @@ -1,5 +1,5 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs b/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs index 0378a5b017..a12d07b8ef 100644 --- a/tests/Avalonia.Base.UnitTests/Utilities/MathUtilitiesTests.cs +++ b/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); + } } }