diff --git a/readme.md b/readme.md
index 8ae3f1ad66..491b517e42 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
-
@@ -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/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs
index b08519963b..912abc6de3 100644
--- a/src/Avalonia.Controls/MenuItem.cs
+++ b/src/Avalonia.Controls/MenuItem.cs
@@ -105,6 +105,7 @@ namespace Avalonia.Controls
static MenuItem()
{
SelectableMixin.Attach(IsSelectedProperty);
+ PressedMixin.Attach();
CommandProperty.Changed.Subscribe(CommandChanged);
FocusableProperty.OverrideDefaultValue(true);
HeaderProperty.Changed.AddClassHandler((x, e) => x.HeaderChanged(e));
@@ -534,11 +535,13 @@ namespace Avalonia.Controls
if (oldValue != null)
{
LogicalChildren.Remove(oldValue);
+ PseudoClasses.Remove(":icon");
}
if (newValue != null)
{
LogicalChildren.Add(newValue);
+ PseudoClasses.Add(":icon");
}
}
@@ -566,11 +569,13 @@ namespace Avalonia.Controls
{
RaiseEvent(new RoutedEventArgs(SubmenuOpenedEvent));
IsSelected = true;
+ PseudoClasses.Add(":open");
}
else
{
CloseSubmenus();
SelectedIndex = -1;
+ PseudoClasses.Remove(":open");
}
}
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/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs
index 1db47a13e7..c91adaa26e 100644
--- a/src/Avalonia.Controls/Primitives/Track.cs
+++ b/src/Avalonia.Controls/Primitives/Track.cs
@@ -259,7 +259,7 @@ namespace Avalonia.Controls.Primitives
CoerceLength(ref increaseButtonLength, arrangeSize.Width);
CoerceLength(ref thumbLength, arrangeSize.Width);
- offset = offset.WithY(isDirectionReversed ? increaseButtonLength + thumbLength : 0.0);
+ offset = offset.WithX(isDirectionReversed ? increaseButtonLength + thumbLength : 0.0);
pieceSize = pieceSize.WithWidth(decreaseButtonLength);
if (DecreaseButton != null)
diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs
index 64378a4eb2..fe1a4f5ac1 100644
--- a/src/Avalonia.Controls/Slider.cs
+++ b/src/Avalonia.Controls/Slider.cs
@@ -201,7 +201,7 @@ namespace Avalonia.Controls
var invert = orient ? 0 : 1;
var calcVal = Math.Abs(invert - logicalPos);
var range = Maximum - Minimum;
- var finalValue = calcVal * range;
+ var finalValue = calcVal * range + Minimum;
Value = IsSnapToTickEnabled ? SnapToTick(finalValue) : finalValue;
}
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/BaseDark.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml
index 44318ffa8f..0bea6c5781 100644
--- a/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml
+++ b/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml
@@ -138,6 +138,9 @@
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml
index e43a7ab4e7..ef296faa60 100644
--- a/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml
+++ b/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml
@@ -138,6 +138,9 @@
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml
index 07f93a3a17..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/src/Avalonia.Themes.Fluent/ContextMenu.xaml b/src/Avalonia.Themes.Fluent/ContextMenu.xaml
index 75f8f7c23d..44783a8dea 100644
--- a/src/Avalonia.Themes.Fluent/ContextMenu.xaml
+++ b/src/Avalonia.Themes.Fluent/ContextMenu.xaml
@@ -1,22 +1,61 @@
-
\ No newline at end of file
+
diff --git a/src/Avalonia.Themes.Fluent/MenuItem.xaml b/src/Avalonia.Themes.Fluent/MenuItem.xaml
index 314416cda0..fbb994e90c 100644
--- a/src/Avalonia.Themes.Fluent/MenuItem.xaml
+++ b/src/Avalonia.Themes.Fluent/MenuItem.xaml
@@ -2,98 +2,143 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="clr-namespace:Avalonia.Controls.Converters;assembly=Avalonia.Controls"
xmlns:sys="clr-namespace:System;assembly=netstandard">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+ 0,4,0,4
+ 0,0,12,0
+ 24,0,0,0
+ M 1,0 10,10 l -9,10 -1,-1 L 8,10 -0,1 Z
+
-
-
-
+
+
+
+
+
+
-
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.Themes.Fluent/Separator.xaml b/src/Avalonia.Themes.Fluent/Separator.xaml
index cf0db16ee6..dc968fe86c 100644
--- a/src/Avalonia.Themes.Fluent/Separator.xaml
+++ b/src/Avalonia.Themes.Fluent/Separator.xaml
@@ -4,17 +4,15 @@
-
-
-
diff --git a/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml b/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml
new file mode 100644
index 0000000000..ed172b52ab
--- /dev/null
+++ b/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml
@@ -0,0 +1,294 @@
+
+
+ 0,0,0,6
+ 6
+ 6
+ 154
+ 20
+ 0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Avalonia.X11/XI2Manager.cs b/src/Avalonia.X11/XI2Manager.cs
index 0734532d92..4e44f55fe0 100644
--- a/src/Avalonia.X11/XI2Manager.cs
+++ b/src/Avalonia.X11/XI2Manager.cs
@@ -97,7 +97,7 @@ namespace Avalonia.X11
{
_platform = platform;
_x11 = platform.Info;
- _multitouch = platform.Options?.EnableMultiTouch ?? false;
+ _multitouch = platform.Options?.EnableMultiTouch ?? true;
var devices =(XIDeviceInfo*) XIQueryDevice(_x11.Display,
(int)XiPredefinedDeviceId.XIAllMasterDevices, out int num);
for (var c = 0; c < num; c++)
@@ -237,6 +237,22 @@ namespace Avalonia.X11
RawPointerEventType.Move, ev.Position, ev.Modifiers));
}
+ if (ev.Type == XiEventType.XI_ButtonPress && ev.Button >= 4 && ev.Button <= 7 && !ev.Emulated)
+ {
+ Vector? scrollDelta = ev.Button switch
+ {
+ 4 => new Vector(0, 1),
+ 5 => new Vector(0, -1),
+ 6 => new Vector(1, 0),
+ 7 => new Vector(-1, 0),
+ _ => null
+ };
+
+ if (scrollDelta.HasValue)
+ client.ScheduleXI2Input(new RawMouseWheelEventArgs(client.MouseDevice, ev.Timestamp,
+ client.InputRoot, ev.Position, scrollDelta.Value, ev.Modifiers));
+ }
+
if (ev.Type == XiEventType.XI_ButtonPress || ev.Type == XiEventType.XI_ButtonRelease)
{
var down = ev.Type == XiEventType.XI_ButtonPress;
diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs
index 0cf5a73b9f..81340fcde7 100644
--- a/src/Windows/Avalonia.Win32/WindowImpl.cs
+++ b/src/Windows/Avalonia.Win32/WindowImpl.cs
@@ -577,7 +577,7 @@ namespace Avalonia.Win32
Handle = new PlatformHandle(_hwnd, PlatformConstants.WindowHandleType);
- _multitouch = Win32Platform.Options.EnableMultitouch ?? false;
+ _multitouch = Win32Platform.Options.EnableMultitouch ?? true;
if (_multitouch)
{