committed by
GitHub
68 changed files with 1903 additions and 705 deletions
@ -1,112 +0,0 @@ |
|||
using System; |
|||
using Android.Content; |
|||
using Android.Graphics; |
|||
using Android.Runtime; |
|||
using Android.Views; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Android.Platform.SkiaPlatform |
|||
{ |
|||
class PopupImpl : TopLevelImpl, IPopupImpl |
|||
{ |
|||
private PixelPoint _position; |
|||
private bool _isAdded; |
|||
Action IWindowBaseImpl.Activated { get; set; } |
|||
public Action<PixelPoint> PositionChanged { get; set; } |
|||
public Action Deactivated { get; set; } |
|||
|
|||
public PopupImpl() : base(ActivityTracker.Current, true) |
|||
{ |
|||
} |
|||
|
|||
private Size _clientSize = new Size(1, 1); |
|||
|
|||
public void Resize(Size value) |
|||
{ |
|||
if (View == null) |
|||
return; |
|||
_clientSize = value; |
|||
UpdateParams(); |
|||
} |
|||
|
|||
public void SetMinMaxSize(Size minSize, Size maxSize) |
|||
{ |
|||
} |
|||
|
|||
public IScreenImpl Screen { get; } |
|||
|
|||
public PixelPoint Position |
|||
{ |
|||
get { return _position; } |
|||
set |
|||
{ |
|||
_position = value; |
|||
PositionChanged?.Invoke(_position); |
|||
UpdateParams(); |
|||
} |
|||
} |
|||
|
|||
WindowManagerLayoutParams CreateParams() => new WindowManagerLayoutParams(0, |
|||
WindowManagerFlags.NotTouchModal, Format.Translucent) |
|||
{ |
|||
Gravity = GravityFlags.Left | GravityFlags.Top, |
|||
WindowAnimations = 0, |
|||
X = (int) _position.X, |
|||
Y = (int) _position.Y, |
|||
Width = Math.Max(1, (int) _clientSize.Width), |
|||
Height = Math.Max(1, (int) _clientSize.Height) |
|||
}; |
|||
|
|||
void UpdateParams() |
|||
{ |
|||
if (_isAdded) |
|||
ActivityTracker.Current?.WindowManager?.UpdateViewLayout(View, CreateParams()); |
|||
} |
|||
|
|||
public override void Show() |
|||
{ |
|||
if (_isAdded) |
|||
return; |
|||
ActivityTracker.Current.WindowManager.AddView(View, CreateParams()); |
|||
_isAdded = true; |
|||
} |
|||
|
|||
public override void Hide() |
|||
{ |
|||
if (_isAdded) |
|||
{ |
|||
var wm = View.Context.ApplicationContext.GetSystemService(Context.WindowService) |
|||
.JavaCast<IWindowManager>(); |
|||
wm.RemoveView(View); |
|||
_isAdded = false; |
|||
} |
|||
} |
|||
|
|||
public override void Dispose() |
|||
{ |
|||
Hide(); |
|||
base.Dispose(); |
|||
} |
|||
|
|||
|
|||
public void Activate() |
|||
{ |
|||
} |
|||
|
|||
public void BeginMoveDrag() |
|||
{ |
|||
//Not supported
|
|||
} |
|||
|
|||
public void BeginResizeDrag(WindowEdge edge) |
|||
{ |
|||
//Not supported
|
|||
} |
|||
|
|||
public void SetTopmost(bool value) |
|||
{ |
|||
//Not supported
|
|||
} |
|||
} |
|||
} |
|||
@ -1,42 +0,0 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using Avalonia.LogicalTree; |
|||
|
|||
namespace Avalonia.Controls.Primitives |
|||
{ |
|||
public class AdornerDecorator : Decorator |
|||
{ |
|||
public AdornerDecorator() |
|||
{ |
|||
AdornerLayer = new AdornerLayer(); |
|||
((ISetLogicalParent)AdornerLayer).SetParent(this); |
|||
AdornerLayer.ZIndex = int.MaxValue; |
|||
VisualChildren.Add(AdornerLayer); |
|||
} |
|||
|
|||
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) |
|||
{ |
|||
base.OnAttachedToLogicalTree(e); |
|||
|
|||
((ILogical)AdornerLayer).NotifyAttachedToLogicalTree(e); |
|||
} |
|||
|
|||
public AdornerLayer AdornerLayer |
|||
{ |
|||
get; |
|||
} |
|||
|
|||
protected override Size MeasureOverride(Size availableSize) |
|||
{ |
|||
AdornerLayer.Measure(availableSize); |
|||
return base.MeasureOverride(availableSize); |
|||
} |
|||
|
|||
protected override Size ArrangeOverride(Size finalSize) |
|||
{ |
|||
AdornerLayer.Arrange(new Rect(finalSize)); |
|||
return base.ArrangeOverride(finalSize); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,26 @@ |
|||
using System; |
|||
using Avalonia.Controls.Presenters; |
|||
using Avalonia.Controls.Primitives.PopupPositioning; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Controls.Primitives |
|||
{ |
|||
public interface IPopupHost : IDisposable |
|||
{ |
|||
void SetChild(IControl control); |
|||
IContentPresenter Presenter { get; } |
|||
IVisual HostedVisualTreeRoot { get; } |
|||
|
|||
event EventHandler<TemplateAppliedEventArgs> TemplateApplied; |
|||
|
|||
void ConfigurePosition(IVisual target, PlacementMode placement, Point offset, |
|||
PopupPositioningEdge anchor = PopupPositioningEdge.None, |
|||
PopupPositioningEdge gravity = PopupPositioningEdge.None); |
|||
void Show(); |
|||
void Hide(); |
|||
IDisposable BindConstraints(AvaloniaObject popup, StyledProperty<double> widthProperty, |
|||
StyledProperty<double> minWidthProperty, StyledProperty<double> maxWidthProperty, |
|||
StyledProperty<double> heightProperty, StyledProperty<double> minHeightProperty, |
|||
StyledProperty<double> maxHeightProperty, StyledProperty<bool> topmostProperty); |
|||
} |
|||
} |
|||
@ -0,0 +1,38 @@ |
|||
using System.Linq; |
|||
using Avalonia.Rendering; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Controls.Primitives |
|||
{ |
|||
public class OverlayLayer : Canvas, ICustomSimpleHitTest |
|||
{ |
|||
public Size AvailableSize { get; private set; } |
|||
public static OverlayLayer GetOverlayLayer(IVisual visual) |
|||
{ |
|||
foreach(var v in visual.GetVisualAncestors()) |
|||
if(v is VisualLayerManager vlm) |
|||
if (vlm.OverlayLayer != null) |
|||
return vlm.OverlayLayer; |
|||
if (visual is TopLevel tl) |
|||
{ |
|||
var layers = tl.GetVisualDescendants().OfType<VisualLayerManager>().FirstOrDefault(); |
|||
return layers?.OverlayLayer; |
|||
} |
|||
|
|||
return null; |
|||
} |
|||
|
|||
public bool HitTest(Point point) |
|||
{ |
|||
return Children.Any(ctrl => ctrl.TransformedBounds?.Contains(point) == true); |
|||
} |
|||
|
|||
protected override Size ArrangeOverride(Size finalSize) |
|||
{ |
|||
// We are saving it here since child controls might need to know the entire size of the overlay
|
|||
// and Bounds won't be updated in time
|
|||
AvailableSize = finalSize; |
|||
return base.ArrangeOverride(finalSize); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,149 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Reactive.Disposables; |
|||
using Avalonia.Controls.Primitives.PopupPositioning; |
|||
using Avalonia.Interactivity; |
|||
using Avalonia.Media; |
|||
using Avalonia.Threading; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Controls.Primitives |
|||
{ |
|||
public class OverlayPopupHost : ContentControl, IPopupHost, IInteractive, IManagedPopupPositionerPopup |
|||
{ |
|||
private readonly OverlayLayer _overlayLayer; |
|||
private PopupPositionerParameters _positionerParameters = new PopupPositionerParameters(); |
|||
private ManagedPopupPositioner _positioner; |
|||
private Point _lastRequestedPosition; |
|||
private bool _shown; |
|||
|
|||
public OverlayPopupHost(OverlayLayer overlayLayer) |
|||
{ |
|||
_overlayLayer = overlayLayer; |
|||
_positioner = new ManagedPopupPositioner(this); |
|||
} |
|||
|
|||
public void SetChild(IControl control) |
|||
{ |
|||
Content = control; |
|||
} |
|||
|
|||
public IVisual HostedVisualTreeRoot => null; |
|||
|
|||
/// <inheritdoc/>
|
|||
IInteractive IInteractive.InteractiveParent => Parent; |
|||
|
|||
public void Dispose() => Hide(); |
|||
|
|||
|
|||
public void Show() |
|||
{ |
|||
_overlayLayer.Children.Add(this); |
|||
_shown = true; |
|||
} |
|||
|
|||
public void Hide() |
|||
{ |
|||
_overlayLayer.Children.Remove(this); |
|||
_shown = false; |
|||
} |
|||
|
|||
public IDisposable BindConstraints(AvaloniaObject popup, StyledProperty<double> widthProperty, StyledProperty<double> minWidthProperty, |
|||
StyledProperty<double> maxWidthProperty, StyledProperty<double> heightProperty, StyledProperty<double> minHeightProperty, |
|||
StyledProperty<double> maxHeightProperty, StyledProperty<bool> topmostProperty) |
|||
{ |
|||
// Topmost property is not supported
|
|||
var bindings = new List<IDisposable>(); |
|||
|
|||
void Bind(AvaloniaProperty what, AvaloniaProperty to) => bindings.Add(this.Bind(what, popup[~to])); |
|||
Bind(WidthProperty, widthProperty); |
|||
Bind(MinWidthProperty, minWidthProperty); |
|||
Bind(MaxWidthProperty, maxWidthProperty); |
|||
Bind(HeightProperty, heightProperty); |
|||
Bind(MinHeightProperty, minHeightProperty); |
|||
Bind(MaxHeightProperty, maxHeightProperty); |
|||
|
|||
return Disposable.Create(() => |
|||
{ |
|||
foreach (var x in bindings) |
|||
x.Dispose(); |
|||
}); |
|||
} |
|||
|
|||
public void ConfigurePosition(IVisual target, PlacementMode placement, Point offset, |
|||
PopupPositioningEdge anchor = PopupPositioningEdge.None, PopupPositioningEdge gravity = PopupPositioningEdge.None) |
|||
{ |
|||
_positionerParameters.ConfigurePosition((TopLevel)_overlayLayer.GetVisualRoot(), target, placement, offset, anchor, |
|||
gravity); |
|||
UpdatePosition(); |
|||
} |
|||
|
|||
protected override Size ArrangeOverride(Size finalSize) |
|||
{ |
|||
if (_positionerParameters.Size != finalSize) |
|||
{ |
|||
_positionerParameters.Size = finalSize; |
|||
UpdatePosition(); |
|||
} |
|||
return base.ArrangeOverride(finalSize); |
|||
} |
|||
|
|||
|
|||
private void UpdatePosition() |
|||
{ |
|||
// Don't bother the positioner with layout system artifacts
|
|||
if (_positionerParameters.Size.Width == 0 || _positionerParameters.Size.Height == 0) |
|||
return; |
|||
if (_shown) |
|||
{ |
|||
_positioner.Update(_positionerParameters); |
|||
} |
|||
} |
|||
|
|||
IReadOnlyList<ManagedPopupPositionerScreenInfo> IManagedPopupPositionerPopup.Screens |
|||
{ |
|||
get |
|||
{ |
|||
var rc = new Rect(default, _overlayLayer.AvailableSize); |
|||
return new[] {new ManagedPopupPositionerScreenInfo(rc, rc)}; |
|||
} |
|||
} |
|||
|
|||
Rect IManagedPopupPositionerPopup.ParentClientAreaScreenGeometry => |
|||
new Rect(default, _overlayLayer.Bounds.Size); |
|||
|
|||
void IManagedPopupPositionerPopup.MoveAndResize(Point devicePoint, Size virtualSize) |
|||
{ |
|||
_lastRequestedPosition = devicePoint; |
|||
Dispatcher.UIThread.Post(() => |
|||
{ |
|||
OverlayLayer.SetLeft(this, _lastRequestedPosition.X); |
|||
OverlayLayer.SetTop(this, _lastRequestedPosition.Y); |
|||
}, DispatcherPriority.Layout); |
|||
} |
|||
|
|||
Point IManagedPopupPositionerPopup.TranslatePoint(Point pt) => pt; |
|||
|
|||
Size IManagedPopupPositionerPopup.TranslateSize(Size size) => size; |
|||
|
|||
public static IPopupHost CreatePopupHost(IVisual target, IAvaloniaDependencyResolver dependencyResolver) |
|||
{ |
|||
var platform = (target.GetVisualRoot() as TopLevel)?.PlatformImpl?.CreatePopup(); |
|||
if (platform != null) |
|||
return new PopupRoot((TopLevel)target.GetVisualRoot(), platform, dependencyResolver); |
|||
|
|||
var overlayLayer = OverlayLayer.GetOverlayLayer(target); |
|||
if (overlayLayer == null) |
|||
throw new InvalidOperationException( |
|||
"Unable to create IPopupImpl and no overlay layer is found for the target control"); |
|||
|
|||
|
|||
return new OverlayPopupHost(overlayLayer); |
|||
} |
|||
|
|||
public override void Render(DrawingContext context) |
|||
{ |
|||
context.FillRectangle(Brushes.White, new Rect(default, Bounds.Size)); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,358 @@ |
|||
// The documentation and flag names in this file are initially taken from
|
|||
// xdg_shell wayland protocol this API is designed after
|
|||
// therefore, I'm including the license from wayland-protocols repo
|
|||
|
|||
/* |
|||
Copyright © 2008-2013 Kristian Høgsberg |
|||
Copyright © 2010-2013 Intel Corporation |
|||
Copyright © 2013 Rafael Antognolli |
|||
Copyright © 2013 Jasper St. Pierre |
|||
Copyright © 2014 Jonas Ådahl |
|||
Copyright © 2014 Jason Ekstrand |
|||
Copyright © 2014-2015 Collabora, Ltd. |
|||
Copyright © 2015 Red Hat Inc. |
|||
|
|||
Permission is hereby granted, free of charge, to any person obtaining a |
|||
copy of this software and associated documentation files (the "Software"), |
|||
to deal in the Software without restriction, including without limitation |
|||
the rights to use, copy, modify, merge, publish, distribute, sublicense, |
|||
and/or sell copies of the Software, and to permit persons to whom the |
|||
Software is furnished to do so, subject to the following conditions: |
|||
|
|||
The above copyright notice and this permission notice (including the next |
|||
paragraph) shall be included in all copies or substantial portions of the |
|||
Software. |
|||
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL |
|||
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
|||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
|||
DEALINGS IN THE SOFTWARE. |
|||
|
|||
--- |
|||
|
|||
The above is the version of the MIT "Expat" License used by X.org: |
|||
|
|||
http://cgit.freedesktop.org/xorg/xserver/tree/COPYING
|
|||
|
|||
|
|||
Adjustments for Avalonia needs: |
|||
Copyright © 2019 Nikita Tsukanov |
|||
|
|||
|
|||
*/ |
|||
|
|||
using System; |
|||
using Avalonia.VisualTree; |
|||
|
|||
namespace Avalonia.Controls.Primitives.PopupPositioning |
|||
{ |
|||
/// <summary>
|
|||
///
|
|||
/// The IPopupPositioner provides a collection of rules for the placement of a
|
|||
/// a popup relative to its parent. Rules can be defined to ensure
|
|||
/// the popup remains within the visible area's borders, and to
|
|||
/// specify how the popup changes its position, such as sliding along
|
|||
/// an axis, or flipping around a rectangle. These positioner-created rules are
|
|||
/// constrained by the requirement that a popup must intersect with or
|
|||
/// be at least partially adjacent to its parent surface.
|
|||
/// </summary>
|
|||
public struct PopupPositionerParameters |
|||
{ |
|||
private PopupPositioningEdge _gravity; |
|||
private PopupPositioningEdge _anchor; |
|||
|
|||
/// <summary>
|
|||
/// Set the size of the popup that is to be positioned with the positioner
|
|||
/// object. The size is in scaled coordinates.
|
|||
/// </summary>
|
|||
public Size Size { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Specify the anchor rectangle within the parent that the popup
|
|||
/// will be placed relative to. The rectangle is relative to the
|
|||
/// parent geometry
|
|||
///
|
|||
/// The anchor rectangle may not extend outside the window geometry of the
|
|||
/// popup's parent. The anchor rectangle is in scaled coordinates
|
|||
/// </summary>
|
|||
public Rect AnchorRectangle { get; set; } |
|||
|
|||
|
|||
/// <summary>
|
|||
/// Defines the anchor point for the anchor rectangle. The specified anchor
|
|||
/// is used derive an anchor point that the popup will be
|
|||
/// positioned relative to. If a corner anchor is set (e.g. 'TopLeft' or
|
|||
/// 'BottomRight'), the anchor point will be at the specified corner;
|
|||
/// otherwise, the derived anchor point will be centered on the specified
|
|||
/// edge, or in the center of the anchor rectangle if no edge is specified.
|
|||
/// </summary>
|
|||
public PopupPositioningEdge Anchor |
|||
{ |
|||
get => _anchor; |
|||
set |
|||
{ |
|||
PopupPositioningEdgeHelper.ValidateEdge(value); |
|||
_anchor = value; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Defines in what direction a popup should be positioned, relative to
|
|||
/// the anchor point of the parent. If a corner gravity is
|
|||
/// specified (e.g. 'BottomRight' or 'TopLeft'), then the popup
|
|||
/// will be placed towards the specified gravity; otherwise, the popup
|
|||
/// will be centered over the anchor point on any axis that had no
|
|||
/// gravity specified.
|
|||
/// </summary>
|
|||
public PopupPositioningEdge Gravity |
|||
{ |
|||
get => _gravity; |
|||
set |
|||
{ |
|||
PopupPositioningEdgeHelper.ValidateEdge(value); |
|||
_gravity = value; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Specify how the popup should be positioned if the originally intended
|
|||
/// position caused the popup to be constrained, meaning at least
|
|||
/// partially outside positioning boundaries set by the positioner. The
|
|||
/// adjustment is set by constructing a bitmask describing the adjustment to
|
|||
/// be made when the popup is constrained on that axis.
|
|||
///
|
|||
/// 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.
|
|||
///
|
|||
/// The default adjustment is none.
|
|||
/// </summary>
|
|||
public PopupPositionerConstraintAdjustment ConstraintAdjustment { get; set; } |
|||
|
|||
/// <summary>
|
|||
/// Specify the popup position offset relative to the position of the
|
|||
/// anchor on the anchor rectangle and the anchor on the popup. For
|
|||
/// example if the anchor of the anchor rectangle is at (x, y), the popup
|
|||
/// has the gravity bottom|right, and the offset is (ox, oy), the calculated
|
|||
/// surface position will be (x + ox, y + oy). The offset position of the
|
|||
/// surface is the one used for constraint testing. See
|
|||
/// set_constraint_adjustment.
|
|||
///
|
|||
/// An example use case is placing a popup menu on top of a user interface
|
|||
/// element, while aligning the user interface element of the parent surface
|
|||
/// with some user interface element placed somewhere in the popup.
|
|||
/// </summary>
|
|||
public Point Offset { get; set; } |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// The constraint adjustment value define ways how popup position will
|
|||
/// be adjusted if the unadjusted position would result in the popup
|
|||
/// being partly constrained.
|
|||
///
|
|||
/// Whether a popup is considered 'constrained' is left to the positioner
|
|||
/// to determine. For example, the popup may be partly outside the
|
|||
/// target platform defined 'work area', thus necessitating the popup's
|
|||
/// position be adjusted until it is entirely inside the work area.
|
|||
/// </summary>
|
|||
[Flags] |
|||
public enum PopupPositionerConstraintAdjustment |
|||
{ |
|||
/// <summary>
|
|||
/// Don't alter the surface position even if it is constrained on some
|
|||
/// axis, for example partially outside the edge of an output.
|
|||
/// </summary>
|
|||
None = 0, |
|||
|
|||
/// <summary>
|
|||
/// Slide the surface along the x axis until it is no longer constrained.
|
|||
/// First try to slide towards the direction of the gravity on the x axis
|
|||
/// until either the edge in the opposite direction of the gravity is
|
|||
/// unconstrained or the edge in the direction of the gravity is
|
|||
/// constrained.
|
|||
///
|
|||
/// Then try to slide towards the opposite direction of the gravity on the
|
|||
/// x axis until either the edge in the direction of the gravity is
|
|||
/// unconstrained or the edge in the opposite direction of the gravity is
|
|||
/// constrained.
|
|||
/// </summary>
|
|||
SlideX = 1, |
|||
|
|||
|
|||
/// <summary>
|
|||
/// Slide the surface along the y axis until it is no longer constrained.
|
|||
///
|
|||
/// First try to slide towards the direction of the gravity on the y axis
|
|||
/// until either the edge in the opposite direction of the gravity is
|
|||
/// unconstrained or the edge in the direction of the gravity is
|
|||
/// constrained.
|
|||
///
|
|||
/// Then try to slide towards the opposite direction of the gravity on the
|
|||
/// y axis until either the edge in the direction of the gravity is
|
|||
/// unconstrained or the edge in the opposite direction of the gravity is
|
|||
/// constrained.
|
|||
/// */
|
|||
/// </summary>
|
|||
SlideY = 2, |
|||
|
|||
/// <summary>
|
|||
/// Invert the anchor and gravity on the x axis if the surface is
|
|||
/// constrained on the x axis. For example, if the left edge of the
|
|||
/// surface is constrained, the gravity is 'left' and the anchor is
|
|||
/// 'left', change the gravity to 'right' and the anchor to 'right'.
|
|||
///
|
|||
/// If the adjusted position also ends up being constrained, the resulting
|
|||
/// position of the flip_x adjustment will be the one before the
|
|||
/// adjustment.
|
|||
/// </summary>
|
|||
FlipX = 4, |
|||
|
|||
/// <summary>
|
|||
/// Invert the anchor and gravity on the y axis if the surface is
|
|||
/// constrained on the y axis. For example, if the bottom edge of the
|
|||
/// surface is constrained, the gravity is 'bottom' and the anchor is
|
|||
/// 'bottom', change the gravity to 'top' and the anchor to 'top'.
|
|||
///
|
|||
/// The adjusted position is calculated given the original anchor
|
|||
/// rectangle and offset, but with the new flipped anchor and gravity
|
|||
/// values.
|
|||
///
|
|||
/// If the adjusted position also ends up being constrained, the resulting
|
|||
/// position of the flip_y adjustment will be the one before the
|
|||
/// adjustment.
|
|||
/// </summary>
|
|||
FlipY = 8, |
|||
All = SlideX|SlideY|FlipX|FlipY |
|||
} |
|||
|
|||
static class PopupPositioningEdgeHelper |
|||
{ |
|||
public static void ValidateEdge(this PopupPositioningEdge edge) |
|||
{ |
|||
if (((edge & PopupPositioningEdge.Left) != 0 && (edge & PopupPositioningEdge.Right) != 0) |
|||
|| |
|||
((edge & PopupPositioningEdge.Top) != 0 && (edge & PopupPositioningEdge.Bottom) != 0)) |
|||
throw new ArgumentException("Opposite edges specified"); |
|||
} |
|||
|
|||
public static PopupPositioningEdge Flip(this PopupPositioningEdge edge) |
|||
{ |
|||
var hmask = PopupPositioningEdge.Left | PopupPositioningEdge.Right; |
|||
var vmask = PopupPositioningEdge.Top | PopupPositioningEdge.Bottom; |
|||
if ((edge & hmask) != 0) |
|||
edge ^= hmask; |
|||
if ((edge & vmask) != 0) |
|||
edge ^= vmask; |
|||
return edge; |
|||
} |
|||
|
|||
public static PopupPositioningEdge FlipX(this PopupPositioningEdge edge) |
|||
{ |
|||
if ((edge & PopupPositioningEdge.HorizontalMask) != 0) |
|||
edge ^= PopupPositioningEdge.HorizontalMask; |
|||
return edge; |
|||
} |
|||
|
|||
public static PopupPositioningEdge FlipY(this PopupPositioningEdge edge) |
|||
{ |
|||
if ((edge & PopupPositioningEdge.VerticalMask) != 0) |
|||
edge ^= PopupPositioningEdge.VerticalMask; |
|||
return edge; |
|||
} |
|||
|
|||
} |
|||
|
|||
[Flags] |
|||
public enum PopupPositioningEdge |
|||
{ |
|||
None, |
|||
Top = 1, |
|||
Bottom = 2, |
|||
Left = 4, |
|||
Right = 8, |
|||
TopLeft = Top | Left, |
|||
TopRight = Top | Right, |
|||
BottomLeft = Bottom | Left, |
|||
BottomRight = Bottom | Right, |
|||
|
|||
|
|||
VerticalMask = Top | Bottom, |
|||
HorizontalMask = Left | Right, |
|||
AllMask = VerticalMask|HorizontalMask |
|||
} |
|||
|
|||
public interface IPopupPositioner |
|||
{ |
|||
void Update(PopupPositionerParameters parameters); |
|||
} |
|||
|
|||
static class PopupPositionerExtensions |
|||
{ |
|||
public static void ConfigurePosition(ref this PopupPositionerParameters positionerParameters, |
|||
TopLevel topLevel, |
|||
IVisual target, PlacementMode placement, Point offset, |
|||
PopupPositioningEdge anchor, PopupPositioningEdge gravity) |
|||
{ |
|||
// 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; |
|||
if (placement == PlacementMode.Pointer) |
|||
{ |
|||
positionerParameters.AnchorRectangle = new Rect(pointer, new Size(1, 1)); |
|||
positionerParameters.Anchor = PopupPositioningEdge.BottomRight; |
|||
positionerParameters.Gravity = PopupPositioningEdge.BottomRight; |
|||
} |
|||
else |
|||
{ |
|||
if (target == null) |
|||
throw new InvalidOperationException("Placement mode is not Pointer and PlacementTarget is null"); |
|||
var matrix = target.TransformToVisual(topLevel); |
|||
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"); |
|||
} |
|||
|
|||
positionerParameters.AnchorRectangle = new Rect(default, target.Bounds.Size) |
|||
.TransformToAABB(matrix.Value); |
|||
|
|||
if (placement == PlacementMode.Right) |
|||
{ |
|||
positionerParameters.Anchor = PopupPositioningEdge.TopRight; |
|||
positionerParameters.Gravity = PopupPositioningEdge.BottomRight; |
|||
} |
|||
else if (placement == PlacementMode.Bottom) |
|||
{ |
|||
positionerParameters.Anchor = PopupPositioningEdge.BottomLeft; |
|||
positionerParameters.Gravity = PopupPositioningEdge.BottomRight; |
|||
} |
|||
else if (placement == PlacementMode.Left) |
|||
{ |
|||
positionerParameters.Anchor = PopupPositioningEdge.TopLeft; |
|||
positionerParameters.Gravity = PopupPositioningEdge.BottomLeft; |
|||
} |
|||
else if (placement == PlacementMode.Top) |
|||
{ |
|||
positionerParameters.Anchor = PopupPositioningEdge.TopLeft; |
|||
positionerParameters.Gravity = PopupPositioningEdge.TopRight; |
|||
} |
|||
else if (placement == PlacementMode.AnchorAndGravity) |
|||
{ |
|||
positionerParameters.Anchor = anchor; |
|||
positionerParameters.Gravity = gravity; |
|||
} |
|||
else |
|||
throw new InvalidOperationException("Invalid value for Popup.PlacementMode"); |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -0,0 +1,175 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
|
|||
namespace Avalonia.Controls.Primitives.PopupPositioning |
|||
{ |
|||
public interface IManagedPopupPositionerPopup |
|||
{ |
|||
IReadOnlyList<ManagedPopupPositionerScreenInfo> Screens { get; } |
|||
Rect ParentClientAreaScreenGeometry { get; } |
|||
void MoveAndResize(Point devicePoint, Size virtualSize); |
|||
Point TranslatePoint(Point pt); |
|||
Size TranslateSize(Size size); |
|||
} |
|||
|
|||
public class ManagedPopupPositionerScreenInfo |
|||
{ |
|||
public Rect Bounds { get; } |
|||
public Rect WorkingArea { get; } |
|||
|
|||
public ManagedPopupPositionerScreenInfo(Rect bounds, Rect workingArea) |
|||
{ |
|||
Bounds = bounds; |
|||
WorkingArea = workingArea; |
|||
} |
|||
} |
|||
|
|||
public class ManagedPopupPositioner : IPopupPositioner |
|||
{ |
|||
private readonly IManagedPopupPositionerPopup _popup; |
|||
|
|||
public ManagedPopupPositioner(IManagedPopupPositionerPopup popup) |
|||
{ |
|||
_popup = popup; |
|||
} |
|||
|
|||
|
|||
private static Point GetAnchorPoint(Rect anchorRect, PopupPositioningEdge edge) |
|||
{ |
|||
double x, y; |
|||
if ((edge & PopupPositioningEdge.Left) != 0) |
|||
x = anchorRect.X; |
|||
else if ((edge & PopupPositioningEdge.Right) != 0) |
|||
x = anchorRect.Right; |
|||
else |
|||
x = anchorRect.X + anchorRect.Width / 2; |
|||
|
|||
if ((edge & PopupPositioningEdge.Top) != 0) |
|||
y = anchorRect.Y; |
|||
else if ((edge & PopupPositioningEdge.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) |
|||
{ |
|||
double x, y; |
|||
if ((gravity & PopupPositioningEdge.Left) != 0) |
|||
x = -size.Width; |
|||
else if ((gravity & PopupPositioningEdge.Right) != 0) |
|||
x = 0; |
|||
else |
|||
x = -size.Width / 2; |
|||
|
|||
if ((gravity & PopupPositioningEdge.Top) != 0) |
|||
y = -size.Height; |
|||
else if ((gravity & PopupPositioningEdge.Bottom) != 0) |
|||
y = 0; |
|||
else |
|||
y = -size.Height / 2; |
|||
return anchorPoint + new Point(x, y); |
|||
} |
|||
|
|||
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)); |
|||
} |
|||
|
|||
|
|||
private void Update(Size translatedSize, Size originalSize, |
|||
Rect anchorRect, PopupPositioningEdge anchor, PopupPositioningEdge gravity, |
|||
PopupPositionerConstraintAdjustment constraintAdjustment, Point offset) |
|||
{ |
|||
var parentGeometry = _popup.ParentClientAreaScreenGeometry; |
|||
anchorRect = anchorRect.Translate(parentGeometry.TopLeft); |
|||
|
|||
Rect GetBounds() |
|||
{ |
|||
var screens = _popup.Screens; |
|||
|
|||
var targetScreen = screens.FirstOrDefault(s => s.Bounds.Contains(anchorRect.TopLeft)) |
|||
?? screens.FirstOrDefault(s => s.Bounds.Intersects(anchorRect)) |
|||
?? screens.FirstOrDefault(s => s.Bounds.Contains(parentGeometry.TopLeft)) |
|||
?? screens.FirstOrDefault(s => s.Bounds.Intersects(parentGeometry)) |
|||
?? screens.FirstOrDefault(); |
|||
return targetScreen?.WorkingArea |
|||
?? new Rect(0, 0, double.MaxValue, double.MaxValue); |
|||
} |
|||
|
|||
var bounds = GetBounds(); |
|||
|
|||
bool FitsInBounds(Rect rc, PopupPositioningEdge edge = PopupPositioningEdge.AllMask) |
|||
{ |
|||
if ((edge & PopupPositioningEdge.Left) != 0 |
|||
&& rc.X < bounds.X) |
|||
return false; |
|||
|
|||
if ((edge & PopupPositioningEdge.Top) != 0 |
|||
&& rc.Y < bounds.Y) |
|||
return false; |
|||
|
|||
if ((edge & PopupPositioningEdge.Right) != 0 |
|||
&& rc.Right > bounds.Right) |
|||
return false; |
|||
|
|||
if ((edge & PopupPositioningEdge.Bottom) != 0 |
|||
&& rc.Bottom > bounds.Bottom) |
|||
return false; |
|||
|
|||
return true; |
|||
} |
|||
|
|||
Rect GetUnconstrained(PopupPositioningEdge a, PopupPositioningEdge g) => |
|||
new Rect(Gravitate(GetAnchorPoint(anchorRect, a), translatedSize, g) + offset, translatedSize); |
|||
|
|||
|
|||
var geo = GetUnconstrained(anchor, gravity); |
|||
|
|||
// If flipping geometry and anchor is allowed and helps, use the flipped one,
|
|||
// otherwise leave it as is
|
|||
if (!FitsInBounds(geo, PopupPositioningEdge.HorizontalMask) |
|||
&& (constraintAdjustment & PopupPositionerConstraintAdjustment.FlipX) != 0) |
|||
{ |
|||
var flipped = GetUnconstrained(anchor.FlipX(), gravity.FlipX()); |
|||
if (FitsInBounds(flipped, PopupPositioningEdge.HorizontalMask)) |
|||
geo = geo.WithX(flipped.X); |
|||
} |
|||
|
|||
// If sliding is allowed, try moving the rect into the bounds
|
|||
if ((constraintAdjustment & PopupPositionerConstraintAdjustment.SlideX) != 0) |
|||
{ |
|||
geo = geo.WithX(Math.Max(geo.X, bounds.X)); |
|||
if (geo.Right > bounds.Right) |
|||
geo = geo.WithX(bounds.Right - geo.Width); |
|||
} |
|||
|
|||
// If flipping geometry and anchor is allowed and helps, use the flipped one,
|
|||
// otherwise leave it as is
|
|||
if (!FitsInBounds(geo, PopupPositioningEdge.VerticalMask) |
|||
&& (constraintAdjustment & PopupPositionerConstraintAdjustment.FlipY) != 0) |
|||
{ |
|||
var flipped = GetUnconstrained(anchor.FlipY(), gravity.FlipY()); |
|||
if (FitsInBounds(flipped, PopupPositioningEdge.VerticalMask)) |
|||
geo = geo.WithY(flipped.Y); |
|||
} |
|||
|
|||
// If sliding is allowed, try moving the rect into the bounds
|
|||
if ((constraintAdjustment & PopupPositionerConstraintAdjustment.SlideY) != 0) |
|||
{ |
|||
geo = geo.WithY(Math.Max(geo.Y, bounds.Y)); |
|||
if (geo.Bottom > bounds.Bottom) |
|||
geo = geo.WithY(bounds.Bottom - geo.Height); |
|||
} |
|||
|
|||
_popup.MoveAndResize(geo.TopLeft, originalSize); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,50 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Linq; |
|||
using Avalonia.Platform; |
|||
|
|||
namespace Avalonia.Controls.Primitives.PopupPositioning |
|||
{ |
|||
/// <summary>
|
|||
/// This class is used to simplify integration of IPopupImpl implementations with popup positioner
|
|||
/// </summary>
|
|||
public class ManagedPopupPositionerPopupImplHelper : IManagedPopupPositionerPopup |
|||
{ |
|||
private readonly IWindowBaseImpl _parent; |
|||
|
|||
public delegate void MoveResizeDelegate(PixelPoint position, Size size, double scaling); |
|||
private readonly MoveResizeDelegate _moveResize; |
|||
|
|||
public ManagedPopupPositionerPopupImplHelper(IWindowBaseImpl parent, MoveResizeDelegate moveResize) |
|||
{ |
|||
_parent = parent; |
|||
_moveResize = moveResize; |
|||
} |
|||
|
|||
public IReadOnlyList<ManagedPopupPositionerScreenInfo> Screens => |
|||
|
|||
_parent.Screen.AllScreens.Select(s => new ManagedPopupPositionerScreenInfo( |
|||
s.Bounds.ToRect(1), s.WorkingArea.ToRect(1))).ToList(); |
|||
|
|||
public Rect ParentClientAreaScreenGeometry |
|||
{ |
|||
get |
|||
{ |
|||
// Popup positioner operates with abstract coordinates, but in our case they are pixel ones
|
|||
var point = _parent.PointToScreen(default); |
|||
var size = PixelSize.FromSize(_parent.ClientSize, _parent.Scaling); |
|||
return new Rect(point.X, point.Y, size.Width, size.Height); |
|||
|
|||
} |
|||
} |
|||
|
|||
public void MoveAndResize(Point devicePoint, Size virtualSize) |
|||
{ |
|||
_moveResize(new PixelPoint((int)devicePoint.X, (int)devicePoint.Y), virtualSize, _parent.Scaling); |
|||
} |
|||
|
|||
public Point TranslatePoint(Point pt) => pt * _parent.Scaling; |
|||
|
|||
public Size TranslateSize(Size size) => size * _parent.Scaling; |
|||
} |
|||
} |
|||
@ -0,0 +1,93 @@ |
|||
using System.Collections.Generic; |
|||
using Avalonia.LogicalTree; |
|||
using Avalonia.Styling; |
|||
|
|||
namespace Avalonia.Controls.Primitives |
|||
{ |
|||
public class VisualLayerManager : Decorator |
|||
{ |
|||
private const int AdornerZIndex = int.MaxValue - 100; |
|||
private const int OverlayZIndex = int.MaxValue - 99; |
|||
private IStyleHost _styleRoot; |
|||
private readonly List<Control> _layers = new List<Control>(); |
|||
|
|||
|
|||
public bool IsPopup { get; set; } |
|||
|
|||
public AdornerLayer AdornerLayer |
|||
{ |
|||
get |
|||
{ |
|||
var rv = FindLayer<AdornerLayer>(); |
|||
if (rv == null) |
|||
AddLayer(rv = new AdornerLayer(), AdornerZIndex); |
|||
return rv; |
|||
} |
|||
} |
|||
|
|||
public OverlayLayer OverlayLayer |
|||
{ |
|||
get |
|||
{ |
|||
if (IsPopup) |
|||
return null; |
|||
var rv = FindLayer<OverlayLayer>(); |
|||
if(rv == null) |
|||
AddLayer(rv = new OverlayLayer(), OverlayZIndex); |
|||
return rv; |
|||
} |
|||
} |
|||
|
|||
T FindLayer<T>() where T : class |
|||
{ |
|||
foreach (var layer in _layers) |
|||
if (layer is T match) |
|||
return match; |
|||
return null; |
|||
} |
|||
|
|||
void AddLayer(Control layer, int zindex) |
|||
{ |
|||
_layers.Add(layer); |
|||
((ISetLogicalParent)layer).SetParent(this); |
|||
layer.ZIndex = zindex; |
|||
VisualChildren.Add(layer); |
|||
if (((ILogical)this).IsAttachedToLogicalTree) |
|||
((ILogical)layer).NotifyAttachedToLogicalTree(new LogicalTreeAttachmentEventArgs(_styleRoot)); |
|||
InvalidateArrange(); |
|||
} |
|||
|
|||
|
|||
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) |
|||
{ |
|||
base.OnAttachedToLogicalTree(e); |
|||
_styleRoot = e.Root; |
|||
|
|||
foreach (var l in _layers) |
|||
((ILogical)l).NotifyAttachedToLogicalTree(e); |
|||
} |
|||
|
|||
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) |
|||
{ |
|||
_styleRoot = null; |
|||
base.OnDetachedFromLogicalTree(e); |
|||
foreach (var l in _layers) |
|||
((ILogical)l).NotifyDetachedFromLogicalTree(e); |
|||
} |
|||
|
|||
|
|||
protected override Size MeasureOverride(Size availableSize) |
|||
{ |
|||
foreach (var l in _layers) |
|||
l.Measure(availableSize); |
|||
return base.MeasureOverride(availableSize); |
|||
} |
|||
|
|||
protected override Size ArrangeOverride(Size finalSize) |
|||
{ |
|||
foreach (var l in _layers) |
|||
l.Arrange(new Rect(finalSize)); |
|||
return base.ArrangeOverride(finalSize); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
<Style xmlns="https://github.com/avaloniaui" Selector="OverlayPopupHost"> |
|||
<Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<VisualLayerManager IsPopup="True" Margin="-1 -1 0 0"> |
|||
<ContentPresenter Name="PART_ContentPresenter" |
|||
Background="{TemplateBinding Background}" |
|||
ContentTemplate="{TemplateBinding ContentTemplate}" |
|||
Content="{TemplateBinding Content}" |
|||
Padding="{TemplateBinding Padding}"/> |
|||
</VisualLayerManager> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</Style> |
|||
@ -0,0 +1,203 @@ |
|||
// Copyright (c) The Avalonia Project. All rights reserved.
|
|||
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|||
|
|||
using System; |
|||
using System.Globalization; |
|||
using Avalonia.Animation.Animators; |
|||
using JetBrains.Annotations; |
|||
|
|||
namespace Avalonia |
|||
{ |
|||
/// <summary>
|
|||
/// Defines a vector.
|
|||
/// </summary>
|
|||
public readonly struct PixelVector |
|||
{ |
|||
/// <summary>
|
|||
/// The X vector.
|
|||
/// </summary>
|
|||
private readonly int _x; |
|||
|
|||
/// <summary>
|
|||
/// The Y vector.
|
|||
/// </summary>
|
|||
private readonly int _y; |
|||
|
|||
/// <summary>
|
|||
/// Initializes a new instance of the <see cref="PixelVector"/> structure.
|
|||
/// </summary>
|
|||
/// <param name="x">The X vector.</param>
|
|||
/// <param name="y">The Y vector.</param>
|
|||
public PixelVector(int x, int y) |
|||
{ |
|||
_x = x; |
|||
_y = y; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the X vector.
|
|||
/// </summary>
|
|||
public int X => _x; |
|||
|
|||
/// <summary>
|
|||
/// Gets the Y vector.
|
|||
/// </summary>
|
|||
public int Y => _y; |
|||
|
|||
/// <summary>
|
|||
/// Converts the <see cref="PixelVector"/> to a <see cref="PixelPoint"/>.
|
|||
/// </summary>
|
|||
/// <param name="a">The vector.</param>
|
|||
public static explicit operator PixelPoint(PixelVector a) |
|||
{ |
|||
return new PixelPoint(a._x, a._y); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Calculates the dot product of two vectors
|
|||
/// </summary>
|
|||
/// <param name="a">First vector</param>
|
|||
/// <param name="b">Second vector</param>
|
|||
/// <returns>The dot product</returns>
|
|||
public static int operator *(PixelVector a, PixelVector b) |
|||
{ |
|||
return a.X * b.X + a.Y * b.Y; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Scales a vector.
|
|||
/// </summary>
|
|||
/// <param name="vector">The vector</param>
|
|||
/// <param name="scale">The scaling factor.</param>
|
|||
/// <returns>The scaled vector.</returns>
|
|||
public static PixelVector operator *(PixelVector vector, int scale) |
|||
{ |
|||
return new PixelVector(vector._x * scale, vector._y * scale); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Scales a vector.
|
|||
/// </summary>
|
|||
/// <param name="vector">The vector</param>
|
|||
/// <param name="scale">The divisor.</param>
|
|||
/// <returns>The scaled vector.</returns>
|
|||
public static PixelVector operator /(PixelVector vector, int scale) |
|||
{ |
|||
return new PixelVector(vector._x / scale, vector._y / scale); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Length of the vector
|
|||
/// </summary>
|
|||
public double Length => Math.Sqrt(X * X + Y * Y); |
|||
|
|||
/// <summary>
|
|||
/// Negates a vector.
|
|||
/// </summary>
|
|||
/// <param name="a">The vector.</param>
|
|||
/// <returns>The negated vector.</returns>
|
|||
public static PixelVector operator -(PixelVector a) |
|||
{ |
|||
return new PixelVector(-a._x, -a._y); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Adds two vectors.
|
|||
/// </summary>
|
|||
/// <param name="a">The first vector.</param>
|
|||
/// <param name="b">The second vector.</param>
|
|||
/// <returns>A vector that is the result of the addition.</returns>
|
|||
public static PixelVector operator +(PixelVector a, PixelVector b) |
|||
{ |
|||
return new PixelVector(a._x + b._x, a._y + b._y); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Subtracts two vectors.
|
|||
/// </summary>
|
|||
/// <param name="a">The first vector.</param>
|
|||
/// <param name="b">The second vector.</param>
|
|||
/// <returns>A vector that is the result of the subtraction.</returns>
|
|||
public static PixelVector operator -(PixelVector a, PixelVector b) |
|||
{ |
|||
return new PixelVector(a._x - b._x, a._y - b._y); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Check if two vectors are equal (bitwise).
|
|||
/// </summary>
|
|||
/// <param name="other"></param>
|
|||
/// <returns></returns>
|
|||
public bool Equals(PixelVector other) |
|||
{ |
|||
return _x == other._x && _y == other._y; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Check if two vectors are nearly equal (numerically).
|
|||
/// </summary>
|
|||
/// <param name="other">The other vector.</param>
|
|||
/// <returns>True if vectors are nearly equal.</returns>
|
|||
[Pure] |
|||
public bool NearlyEquals(PixelVector other) |
|||
{ |
|||
const float tolerance = float.Epsilon; |
|||
|
|||
return Math.Abs(_x - other._x) < tolerance && Math.Abs(_y - other._y) < tolerance; |
|||
} |
|||
|
|||
public override bool Equals(object obj) |
|||
{ |
|||
if (ReferenceEquals(null, obj)) return false; |
|||
|
|||
return obj is PixelVector vector && Equals(vector); |
|||
} |
|||
|
|||
public override int GetHashCode() |
|||
{ |
|||
unchecked |
|||
{ |
|||
return (_x.GetHashCode() * 397) ^ _y.GetHashCode(); |
|||
} |
|||
} |
|||
|
|||
public static bool operator ==(PixelVector left, PixelVector right) |
|||
{ |
|||
return left.Equals(right); |
|||
} |
|||
|
|||
public static bool operator !=(PixelVector left, PixelVector right) |
|||
{ |
|||
return !left.Equals(right); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns the string representation of the point.
|
|||
/// </summary>
|
|||
/// <returns>The string representation of the point.</returns>
|
|||
public override string ToString() |
|||
{ |
|||
return string.Format(CultureInfo.InvariantCulture, "{0}, {1}", _x, _y); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns a new vector with the specified X coordinate.
|
|||
/// </summary>
|
|||
/// <param name="x">The X coordinate.</param>
|
|||
/// <returns>The new vector.</returns>
|
|||
public PixelVector WithX(int x) |
|||
{ |
|||
return new PixelVector(x, _y); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Returns a new vector with the specified Y coordinate.
|
|||
/// </summary>
|
|||
/// <param name="y">The Y coordinate.</param>
|
|||
/// <returns>The new vector.</returns>
|
|||
public PixelVector WithY(int y) |
|||
{ |
|||
return new PixelVector(_x, y); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue