Custom popup placement callback (#15667)
* Add CustomPopupPlacement API * Add Placement="Custom" support for Flyout, ToolTip and ContextMenu controls as well * Adjust some API changes * Add Avalonia.Controls.Primitives.IPopupHost.ConfigurePosition breaking change * Extract new types into separated files * Fix build after merge conflict * Adjust nupkg.xml * Dispose property subscriptions after popup is closed, avoiding flickering * Adjust API to be more future proof and add new parameters. * Add new ContextRequestedEventArgs overload while I am on it
This commit is contained in:
@@ -19,6 +19,12 @@
|
||||
<Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
|
||||
<Right>target/netstandard2.0/Avalonia.Base.dll</Right>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Avalonia.Controls.Primitives.IPopupHost.ConfigurePosition(Avalonia.Visual,Avalonia.Controls.PlacementMode,Avalonia.Point,Avalonia.Controls.Primitives.PopupPositioning.PopupAnchor,Avalonia.Controls.Primitives.PopupPositioning.PopupGravity,Avalonia.Controls.Primitives.PopupPositioning.PopupPositionerConstraintAdjustment,System.Nullable{Avalonia.Rect})</Target>
|
||||
<Left>baseline/netstandard2.0/Avalonia.Controls.dll</Left>
|
||||
<Right>target/netstandard2.0/Avalonia.Controls.dll</Right>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Avalonia.Controls.Screens.#ctor(Avalonia.Platform.IScreenImpl)</Target>
|
||||
@@ -43,6 +49,12 @@
|
||||
<Left>baseline/netstandard2.0/Avalonia.Controls.dll</Left>
|
||||
<Right>target/netstandard2.0/Avalonia.Controls.dll</Right>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0006</DiagnosticId>
|
||||
<Target>M:Avalonia.Controls.Primitives.IPopupHost.ConfigurePosition(Avalonia.Controls.Primitives.PopupPositioning.PopupPositionRequest)</Target>
|
||||
<Left>baseline/netstandard2.0/Avalonia.Controls.dll</Left>
|
||||
<Right>target/netstandard2.0/Avalonia.Controls.dll</Right>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0009</DiagnosticId>
|
||||
<Target>T:Avalonia.Controls.Screens</Target>
|
||||
|
||||
@@ -222,7 +222,15 @@
|
||||
</Flyout>
|
||||
</Button.Flyout>
|
||||
</Button>
|
||||
|
||||
<Button Content="Placement=Custom">
|
||||
<Button.Flyout>
|
||||
<Flyout Placement="Custom" CustomPopupPlacementCallback="CustomPlacementCallback">
|
||||
<Panel Width="100" Height="100">
|
||||
<TextBlock Text="Flyout Content!" />
|
||||
</Panel>
|
||||
</Flyout>
|
||||
</Button.Flyout>
|
||||
</Button>
|
||||
</UniformGrid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
@@ -267,7 +275,6 @@
|
||||
</Flyout>
|
||||
</Button.Flyout>
|
||||
</Button>
|
||||
|
||||
</WrapPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
using System;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Controls.Primitives.PopupPositioning;
|
||||
using Avalonia.Interactivity;
|
||||
|
||||
namespace ControlCatalog.Pages
|
||||
@@ -71,5 +74,25 @@ namespace ControlCatalog.Pages
|
||||
"Then attach the flyout where you want it:\n" +
|
||||
"<Button Content=\"Launch Flyout here\" Flyout=\"{StaticResource SharedFlyout}\" />";
|
||||
}
|
||||
|
||||
public void CustomPlacementCallback(CustomPopupPlacement placement)
|
||||
{
|
||||
var r = new Random().Next();
|
||||
placement.Anchor = (r % 4) switch
|
||||
{
|
||||
1 => PopupAnchor.Top,
|
||||
2 => PopupAnchor.Left,
|
||||
3 => PopupAnchor.Right,
|
||||
_ => PopupAnchor.Bottom,
|
||||
};
|
||||
placement.Gravity = (r % 4) switch
|
||||
{
|
||||
1 => PopupGravity.Top,
|
||||
2 => PopupGravity.Left,
|
||||
3 => PopupGravity.Right,
|
||||
_ => PopupGravity.Bottom,
|
||||
};
|
||||
placement.Offset = new Point(r % 20, r % 20);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,31 +5,24 @@
|
||||
Spacing="4">
|
||||
<TextBlock Classes="h2">A control which pops up a hint when a control is hovered</TextBlock>
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,Auto"
|
||||
ColumnDefinitions="Auto,Auto"
|
||||
Margin="0,16,0,0"
|
||||
HorizontalAlignment="Center">
|
||||
<UniformGrid Columns="2"
|
||||
Margin="0,16,0,0"
|
||||
HorizontalAlignment="Center">
|
||||
<ToggleSwitch Margin="5"
|
||||
HorizontalAlignment="Center"
|
||||
IsChecked="{Binding Path=(ToolTip.ServiceEnabled), RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||
Content="Enable ToolTip service" />
|
||||
<Border Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
Background="{DynamicResource SystemAccentColor}"
|
||||
HorizontalAlignment="Center"
|
||||
IsChecked="{Binding Path=(ToolTip.ServiceEnabled), RelativeSource={RelativeSource AncestorType=UserControl}}"
|
||||
Content="Enable ToolTip service" />
|
||||
<ToggleSwitch Margin="5"
|
||||
IsChecked="{Binding ElementName=Border, Path=(ToolTip.IsOpen)}"
|
||||
HorizontalAlignment="Center"
|
||||
Content="ToolTip Open" />
|
||||
<Border Background="{DynamicResource SystemAccentColor}"
|
||||
Margin="5"
|
||||
Padding="50"
|
||||
ToolTip.Tip="This is a ToolTip">
|
||||
<TextBlock>Hover Here</TextBlock>
|
||||
</Border>
|
||||
<ToggleSwitch Grid.Column="1"
|
||||
Margin="5"
|
||||
Grid.Row="0"
|
||||
IsChecked="{Binding ElementName=Border, Path=(ToolTip.IsOpen)}"
|
||||
HorizontalAlignment="Center"
|
||||
Content="ToolTip Open" />
|
||||
<Border Name="Border"
|
||||
Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
Background="{DynamicResource SystemAccentColor}"
|
||||
Margin="5"
|
||||
Padding="50"
|
||||
@@ -42,8 +35,15 @@
|
||||
</ToolTip.Tip>
|
||||
<TextBlock>ToolTip bottom placement</TextBlock>
|
||||
</Border>
|
||||
<Border Grid.Row="2"
|
||||
Background="{DynamicResource SystemAccentColor}"
|
||||
<Border Background="{DynamicResource SystemAccentColor}"
|
||||
Margin="5"
|
||||
Padding="50"
|
||||
ToolTip.Placement="Custom"
|
||||
ToolTip.CustomPopupPlacementCallback="CustomPlacementCallback"
|
||||
ToolTip.Tip="Custom positioned tooltip">
|
||||
<TextBlock>ToolTip custom placement</TextBlock>
|
||||
</Border>
|
||||
<Border Background="{DynamicResource SystemAccentColor}"
|
||||
Margin="5"
|
||||
Padding="50"
|
||||
ToolTip.Tip="Hello"
|
||||
@@ -67,8 +67,7 @@
|
||||
<TextBlock>Moving offset</TextBlock>
|
||||
</Border>
|
||||
|
||||
<Button Grid.Row="2" Grid.Column="1"
|
||||
IsEnabled="False"
|
||||
<Button IsEnabled="False"
|
||||
ToolTip.ShowOnDisabled="True"
|
||||
ToolTip.Tip="This control is disabled"
|
||||
Margin="5"
|
||||
@@ -76,24 +75,20 @@
|
||||
<TextBlock>ToolTip on a disabled control</TextBlock>
|
||||
</Button>
|
||||
|
||||
<Border Grid.Row="3"
|
||||
Grid.Column="0"
|
||||
Background="{DynamicResource SystemAccentColor}"
|
||||
<Border Background="{DynamicResource SystemAccentColor}"
|
||||
Margin="5"
|
||||
Padding="50"
|
||||
ToolTip.Tip="Outer tooltip">
|
||||
<TextBlock Background="{StaticResource SystemAccentColorDark1}" Padding="10" ToolTip.Tip="Inner tooltip" VerticalAlignment="Center">Nested ToolTips</TextBlock>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Row="3"
|
||||
Grid.Column="1"
|
||||
Background="{DynamicResource SystemAccentColor}"
|
||||
|
||||
<Border Background="{DynamicResource SystemAccentColor}"
|
||||
Margin="5"
|
||||
Padding="50"
|
||||
ToolTip.ToolTipOpening="ToolTipOpening"
|
||||
ToolTip.Tip="Should never be visible">
|
||||
<TextBlock VerticalAlignment="Center">ToolTip replaced on the fly</TextBlock>
|
||||
</Border>
|
||||
</Grid>
|
||||
</UniformGrid>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
using System;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Controls.Primitives.PopupPositioning;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace ControlCatalog.Pages
|
||||
@@ -19,6 +22,27 @@ namespace ControlCatalog.Pages
|
||||
private void ToolTipOpening(object? sender, CancelRoutedEventArgs args)
|
||||
{
|
||||
((Control)args.Source!).SetValue(ToolTip.TipProperty, "New tip set from ToolTipOpening.");
|
||||
}
|
||||
}
|
||||
|
||||
public void CustomPlacementCallback(CustomPopupPlacement placement)
|
||||
{
|
||||
var r = new Random().Next();
|
||||
|
||||
placement.Anchor = (r % 4) switch
|
||||
{
|
||||
1 => PopupAnchor.Top,
|
||||
2 => PopupAnchor.Left,
|
||||
3 => PopupAnchor.Right,
|
||||
_ => PopupAnchor.Bottom,
|
||||
};
|
||||
placement.Gravity = (r % 4) switch
|
||||
{
|
||||
1 => PopupGravity.Top,
|
||||
2 => PopupGravity.Left,
|
||||
3 => PopupGravity.Right,
|
||||
_ => PopupGravity.Bottom,
|
||||
};
|
||||
placement.Offset = new Point(r % 20, r % 20);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,10 @@ namespace Avalonia.Controls
|
||||
public static readonly StyledProperty<Control?> PlacementTargetProperty =
|
||||
Popup.PlacementTargetProperty.AddOwner<ContextMenu>();
|
||||
|
||||
/// <inheritdoc cref="Popup.CustomPopupPlacementCallbackProperty"/>
|
||||
public static readonly StyledProperty<CustomPopupPlacementCallback?> CustomPopupPlacementCallbackProperty =
|
||||
Popup.CustomPopupPlacementCallbackProperty.AddOwner<ContextMenu>();
|
||||
|
||||
private Popup? _popup;
|
||||
private List<Control>? _attachedControls;
|
||||
private IInputElement? _previousFocus;
|
||||
@@ -185,6 +189,13 @@ namespace Avalonia.Controls
|
||||
set => SetValue(PlacementTargetProperty, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Popup.CustomPopupPlacementCallback"/>
|
||||
public CustomPopupPlacementCallback? CustomPopupPlacementCallback
|
||||
{
|
||||
get => GetValue(CustomPopupPlacementCallbackProperty);
|
||||
set => SetValue(CustomPopupPlacementCallbackProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the value of the
|
||||
/// <see cref="P:Avalonia.Controls.ContextMenu.IsOpen" />
|
||||
@@ -340,6 +351,7 @@ namespace Avalonia.Controls
|
||||
_popup.PlacementConstraintAdjustment = PlacementConstraintAdjustment;
|
||||
_popup.PlacementGravity = PlacementGravity;
|
||||
_popup.PlacementRect = PlacementRect;
|
||||
_popup.CustomPopupPlacementCallback = CustomPopupPlacementCallback;
|
||||
_popup.WindowManagerAddShadowHint = WindowManagerAddShadowHint;
|
||||
IsOpen = true;
|
||||
_popup.IsOpen = true;
|
||||
|
||||
@@ -26,6 +26,13 @@ namespace Avalonia.Controls
|
||||
_pointerEventArgs = pointerEventArgs;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ContextRequestedEventArgs()" />
|
||||
public ContextRequestedEventArgs(ContextRequestedEventArgs contextRequestedEventArgs)
|
||||
: this()
|
||||
{
|
||||
_pointerEventArgs = contextRequestedEventArgs._pointerEventArgs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the x- and y-coordinates of the pointer position, optionally evaluated against a coordinate origin of a supplied <see cref="Control"/>.
|
||||
/// </summary>
|
||||
|
||||
@@ -30,11 +30,15 @@ namespace Avalonia.Controls.Primitives
|
||||
/// <inheritdoc cref="Popup.PlacementAnchorProperty"/>
|
||||
public static readonly StyledProperty<PopupAnchor> PlacementAnchorProperty =
|
||||
Popup.PlacementAnchorProperty.AddOwner<PopupFlyoutBase>();
|
||||
|
||||
|
||||
/// <inheritdoc cref="Popup.PlacementAnchorProperty"/>
|
||||
public static readonly StyledProperty<PopupGravity> PlacementGravityProperty =
|
||||
Popup.PlacementGravityProperty.AddOwner<PopupFlyoutBase>();
|
||||
|
||||
/// <inheritdoc cref="Popup.CustomPopupPlacementCallbackProperty"/>
|
||||
public static readonly StyledProperty<CustomPopupPlacementCallback?> CustomPopupPlacementCallbackProperty =
|
||||
Popup.CustomPopupPlacementCallbackProperty.AddOwner<PopupFlyoutBase>();
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="ShowMode"/> property
|
||||
/// </summary>
|
||||
@@ -112,6 +116,13 @@ namespace Avalonia.Controls.Primitives
|
||||
set => SetValue(VerticalOffsetProperty, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Popup.CustomPopupPlacementCallback"/>
|
||||
public CustomPopupPlacementCallback? CustomPopupPlacementCallback
|
||||
{
|
||||
get => GetValue(CustomPopupPlacementCallbackProperty);
|
||||
set => SetValue(CustomPopupPlacementCallbackProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the desired ShowMode
|
||||
/// </summary>
|
||||
@@ -445,6 +456,7 @@ namespace Avalonia.Controls.Primitives
|
||||
Popup.HorizontalOffset = HorizontalOffset;
|
||||
Popup.PlacementAnchor = PlacementAnchor;
|
||||
Popup.PlacementGravity = PlacementGravity;
|
||||
Popup.CustomPopupPlacementCallback = CustomPopupPlacementCallback;
|
||||
if (showAtPointer)
|
||||
{
|
||||
Popup.Placement = PlacementMode.Pointer;
|
||||
|
||||
@@ -81,6 +81,11 @@ namespace Avalonia.Controls
|
||||
/// <summary>
|
||||
/// Preferred location is to the right of the target element, with the bottom edge of popup aligned with bottom edge of the target element.
|
||||
/// </summary>
|
||||
RightEdgeAlignedBottom
|
||||
RightEdgeAlignedBottom,
|
||||
|
||||
/// <summary>
|
||||
/// A position and repositioning behavior that is defined by the <see cref="Popup.CustomPopupPlacementCallback"/> property.
|
||||
/// </summary>
|
||||
Custom
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using Avalonia.Controls.Presenters;
|
||||
using Avalonia.Controls.Primitives.PopupPositioning;
|
||||
using Avalonia.Diagnostics;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Metadata;
|
||||
@@ -17,6 +18,7 @@ namespace Avalonia.Controls.Primitives
|
||||
/// on an <see cref="OverlayLayer"/>.
|
||||
/// </remarks>
|
||||
[NotClientImplementable]
|
||||
[Unstable(ObsoletionMessages.MayBeRemovedInAvalonia12)]
|
||||
public interface IPopupHost : IDisposable, IFocusScope
|
||||
{
|
||||
/// <summary>
|
||||
@@ -79,20 +81,7 @@ namespace Avalonia.Controls.Primitives
|
||||
/// Configures the position of the popup according to a target control and a set of
|
||||
/// placement parameters.
|
||||
/// </summary>
|
||||
/// <param name="target">The placement target.</param>
|
||||
/// <param name="placement">The placement mode.</param>
|
||||
/// <param name="offset">The offset, in device-independent pixels.</param>
|
||||
/// <param name="anchor">The anchor point.</param>
|
||||
/// <param name="gravity">The popup gravity.</param>
|
||||
/// <param name="constraintAdjustment">Defines how a popup position will be adjusted if the unadjusted position would result in the popup being partly constrained.</param>
|
||||
/// <param name="rect">
|
||||
/// The anchor rect. If null, the bounds of <paramref name="target"/> will be used.
|
||||
/// </param>
|
||||
void ConfigurePosition(Visual target, PlacementMode placement, Point offset,
|
||||
PopupAnchor anchor = PopupAnchor.None,
|
||||
PopupGravity gravity = PopupGravity.None,
|
||||
PopupPositionerConstraintAdjustment constraintAdjustment = PopupPositionerConstraintAdjustment.All,
|
||||
Rect? rect = null);
|
||||
void ConfigurePosition(PopupPositionRequest positionRequest);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the control to display in the popup.
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Avalonia.Controls.Primitives.PopupPositioning;
|
||||
using Avalonia.Diagnostics;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Metadata;
|
||||
using Avalonia.Threading;
|
||||
using Avalonia.VisualTree;
|
||||
|
||||
namespace Avalonia.Controls.Primitives
|
||||
@@ -19,9 +19,10 @@ namespace Avalonia.Controls.Primitives
|
||||
|
||||
private readonly OverlayLayer _overlayLayer;
|
||||
private readonly ManagedPopupPositioner _positioner;
|
||||
private PopupPositionerParameters _positionerParameters;
|
||||
private Point _lastRequestedPosition;
|
||||
private bool _shown;
|
||||
private PopupPositionRequest? _popupPositionRequest;
|
||||
private Size _popupSize;
|
||||
private bool _shown, _needsUpdate;
|
||||
|
||||
public OverlayPopupHost(OverlayLayer overlayLayer)
|
||||
{
|
||||
@@ -73,36 +74,42 @@ namespace Avalonia.Controls.Primitives
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[Unstable(ObsoletionMessages.MayBeRemovedInAvalonia12)]
|
||||
public void ConfigurePosition(Visual target, PlacementMode placement, Point offset,
|
||||
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, constraintAdjustment, rect, FlowDirection);
|
||||
((IPopupHost)this).ConfigurePosition(new PopupPositionRequest(target, placement, offset, anchor, gravity,
|
||||
constraintAdjustment, rect, null));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
void IPopupHost.ConfigurePosition(PopupPositionRequest positionRequest)
|
||||
{
|
||||
_popupPositionRequest = positionRequest;
|
||||
_needsUpdate = true;
|
||||
UpdatePosition();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
if (_positionerParameters.Size != finalSize)
|
||||
if (_popupSize != finalSize)
|
||||
{
|
||||
_positionerParameters.Size = finalSize;
|
||||
_popupSize = finalSize;
|
||||
_needsUpdate = true;
|
||||
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)
|
||||
if (_needsUpdate && _popupPositionRequest is not null)
|
||||
{
|
||||
_positioner.Update(_positionerParameters);
|
||||
_needsUpdate = false;
|
||||
_positioner.Update(TopLevel.GetTopLevel(_overlayLayer)!, _popupPositionRequest, _popupSize, FlowDirection);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,9 @@ namespace Avalonia.Controls.Primitives
|
||||
/// </summary>
|
||||
public class Popup : Control, IPopupHostProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Defines the <see cref="WindowManagerAddShadowHint"/> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<bool> WindowManagerAddShadowHintProperty =
|
||||
AvaloniaProperty.Register<Popup, bool>(nameof(WindowManagerAddShadowHint), false);
|
||||
|
||||
@@ -89,9 +92,21 @@ namespace Avalonia.Controls.Primitives
|
||||
public static readonly StyledProperty<Control?> PlacementTargetProperty =
|
||||
AvaloniaProperty.Register<Popup, Control?>(nameof(PlacementTarget));
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="CustomPopupPlacementCallback"/> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<CustomPopupPlacementCallback?> CustomPopupPlacementCallbackProperty =
|
||||
AvaloniaProperty.Register<Popup, CustomPopupPlacementCallback?>(nameof(CustomPopupPlacementCallback));
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="OverlayDismissEventPassThrough"/> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<bool> OverlayDismissEventPassThroughProperty =
|
||||
AvaloniaProperty.Register<Popup, bool>(nameof(OverlayDismissEventPassThrough));
|
||||
|
||||
/// <summary>
|
||||
/// Defines the <see cref="OverlayInputPassThroughElement"/> property.
|
||||
/// </summary>
|
||||
public static readonly StyledProperty<IInputElement?> OverlayInputPassThroughElementProperty =
|
||||
AvaloniaProperty.Register<Popup, IInputElement?>(nameof(OverlayInputPassThroughElement));
|
||||
|
||||
@@ -287,6 +302,15 @@ namespace Avalonia.Controls.Primitives
|
||||
set => SetValue(PlacementTargetProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a delegate handler method that positions the Popup control, when <see cref="Placement"/> is set to <see cref="PlacementMode.Custom"/>.
|
||||
/// </summary>
|
||||
public CustomPopupPlacementCallback? CustomPopupPlacementCallback
|
||||
{
|
||||
get => GetValue(CustomPopupPlacementCallbackProperty);
|
||||
set => SetValue(CustomPopupPlacementCallbackProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the event that closes the popup is passed
|
||||
/// through to the parent window.
|
||||
@@ -603,14 +627,15 @@ namespace Avalonia.Controls.Primitives
|
||||
|
||||
private void UpdateHostPosition(IPopupHost popupHost, Control placementTarget)
|
||||
{
|
||||
popupHost.ConfigurePosition(
|
||||
popupHost.ConfigurePosition(new PopupPositionRequest(
|
||||
placementTarget,
|
||||
Placement,
|
||||
new Point(HorizontalOffset, VerticalOffset),
|
||||
PlacementAnchor,
|
||||
PlacementGravity,
|
||||
PlacementConstraintAdjustment,
|
||||
PlacementRect ?? new Rect(default, placementTarget.Bounds.Size));
|
||||
PlacementRect ?? new Rect(default, placementTarget.Bounds.Size),
|
||||
CustomPopupPlacementCallback));
|
||||
}
|
||||
|
||||
private void UpdateHostSizing(IPopupHost popupHost, TopLevel topLevel, Control placementTarget)
|
||||
@@ -651,14 +676,15 @@ namespace Avalonia.Controls.Primitives
|
||||
var placementTarget = PlacementTarget ?? this.FindLogicalAncestorOfType<Control>();
|
||||
if (placementTarget == null)
|
||||
return;
|
||||
_openState.PopupHost.ConfigurePosition(
|
||||
_openState.PopupHost.ConfigurePosition(new PopupPositionRequest(
|
||||
placementTarget,
|
||||
Placement,
|
||||
new Point(HorizontalOffset, VerticalOffset),
|
||||
PlacementAnchor,
|
||||
PlacementGravity,
|
||||
PlacementConstraintAdjustment,
|
||||
PlacementRect);
|
||||
PlacementRect,
|
||||
CustomPopupPlacementCallback));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
namespace Avalonia.Controls.Primitives.PopupPositioning;
|
||||
|
||||
/// <summary>
|
||||
/// Defines custom placement parameters for a <see cref="CustomPopupPlacementCallback"/> callback.
|
||||
/// </summary>
|
||||
public record CustomPopupPlacement
|
||||
{
|
||||
private PopupGravity _gravity;
|
||||
private PopupAnchor _anchor;
|
||||
|
||||
internal CustomPopupPlacement(Size popupSize, Visual target)
|
||||
{
|
||||
PopupSize = popupSize;
|
||||
Target = target;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="Size"/> of the <see cref="Popup"/> control.
|
||||
/// </summary>
|
||||
public Size PopupSize { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Placement target of the popup.
|
||||
/// </summary>
|
||||
public Visual Target { get; }
|
||||
|
||||
/// <see cref="PopupPositionerParameters.AnchorRectangle"/>
|
||||
public Rect AnchorRectangle { get; set; }
|
||||
|
||||
/// <see cref="PopupPositionerParameters.Anchor"/>
|
||||
public PopupAnchor Anchor
|
||||
{
|
||||
get => _anchor;
|
||||
set
|
||||
{
|
||||
PopupPositioningEdgeHelper.ValidateEdge(value);
|
||||
_anchor = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <see cref="PopupPositionerParameters.Gravity"/>
|
||||
public PopupGravity Gravity
|
||||
{
|
||||
get => _gravity;
|
||||
set
|
||||
{
|
||||
PopupPositioningEdgeHelper.ValidateGravity(value);
|
||||
_gravity = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <see cref="PopupPositionerParameters.ConstraintAdjustment"/>
|
||||
public PopupPositionerConstraintAdjustment ConstraintAdjustment { get; set; }
|
||||
|
||||
/// <see cref="PopupPositionerParameters.Offset"/>
|
||||
public Point Offset { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Avalonia.Controls.Primitives.PopupPositioning;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a method that provides custom positioning for a <see cref="Popup"/> control.
|
||||
/// </summary>
|
||||
public delegate void CustomPopupPlacementCallback(CustomPopupPlacement parameters);
|
||||
@@ -45,6 +45,9 @@ Copyright © 2019 Nikita Tsukanov
|
||||
*/
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using Avalonia.Diagnostics;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Metadata;
|
||||
using Avalonia.VisualTree;
|
||||
@@ -63,7 +66,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
|
||||
/// requirement that a popup must intersect with or be at least partially adjacent to its parent
|
||||
/// surface.
|
||||
/// </remarks>
|
||||
[Unstable]
|
||||
[Unstable(ObsoletionMessages.MayBeRemovedInAvalonia12)]
|
||||
public record struct PopupPositionerParameters
|
||||
{
|
||||
private PopupGravity _gravity;
|
||||
@@ -443,19 +446,35 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
|
||||
void Update(PopupPositionerParameters parameters);
|
||||
}
|
||||
|
||||
[Unstable]
|
||||
static class PopupPositionerExtensions
|
||||
internal static class PopupPositionerExtensions
|
||||
{
|
||||
public static void ConfigurePosition(ref this PopupPositionerParameters positionerParameters,
|
||||
public static void Update(
|
||||
this IPopupPositioner positioner,
|
||||
TopLevel topLevel,
|
||||
Visual target, PlacementMode placement, Point offset,
|
||||
PopupAnchor anchor, PopupGravity gravity,
|
||||
PopupPositionerConstraintAdjustment constraintAdjustment, Rect? rect,
|
||||
PopupPositionRequest positionRequest,
|
||||
Size popupSize,
|
||||
FlowDirection flowDirection)
|
||||
{
|
||||
positionerParameters.Offset = offset;
|
||||
positionerParameters.ConstraintAdjustment = constraintAdjustment;
|
||||
if (placement == PlacementMode.Pointer)
|
||||
if (popupSize == default)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var parameters = BuildParameters(topLevel, positionRequest, popupSize, flowDirection);
|
||||
positioner.Update(parameters);
|
||||
}
|
||||
|
||||
private static PopupPositionerParameters BuildParameters(
|
||||
TopLevel topLevel,
|
||||
PopupPositionRequest positionRequest,
|
||||
Size popupSize,
|
||||
FlowDirection flowDirection)
|
||||
{
|
||||
PopupPositionerParameters positionerParameters = default;
|
||||
positionerParameters.Offset = positionRequest.Offset;
|
||||
positionerParameters.Size = popupSize;
|
||||
positionerParameters.ConstraintAdjustment = positionRequest.ConstraintAdjustment;
|
||||
if (positionRequest.Placement == PlacementMode.Pointer)
|
||||
{
|
||||
// We need a better way for tracking the last pointer position
|
||||
var position = topLevel.PointToClient(topLevel.LastPointerPosition ?? default);
|
||||
@@ -464,39 +483,45 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
|
||||
positionerParameters.Anchor = PopupAnchor.TopLeft;
|
||||
positionerParameters.Gravity = PopupGravity.BottomRight;
|
||||
}
|
||||
else if (positionRequest.Placement == PlacementMode.Custom)
|
||||
{
|
||||
if (positionRequest.PlacementCallback is null)
|
||||
throw new InvalidOperationException(
|
||||
"CustomPopupPlacementCallback property must be set, when Placement=PlacementMode.Custom");
|
||||
|
||||
positionerParameters.AnchorRectangle = CalculateAnchorRect(topLevel, positionRequest);
|
||||
|
||||
var customPlacementParameters = new CustomPopupPlacement(
|
||||
popupSize,
|
||||
positionRequest.Target)
|
||||
{
|
||||
AnchorRectangle = positionerParameters.AnchorRectangle,
|
||||
Anchor = positionerParameters.Anchor,
|
||||
Gravity = positionerParameters.Gravity,
|
||||
ConstraintAdjustment = positionerParameters.ConstraintAdjustment,
|
||||
Offset = positionerParameters.Offset
|
||||
};
|
||||
|
||||
positionRequest.PlacementCallback.Invoke(customPlacementParameters);
|
||||
|
||||
positionerParameters.AnchorRectangle = customPlacementParameters.AnchorRectangle;
|
||||
positionerParameters.Anchor = customPlacementParameters.Anchor;
|
||||
positionerParameters.Gravity = customPlacementParameters.Gravity;
|
||||
positionerParameters.ConstraintAdjustment = customPlacementParameters.ConstraintAdjustment;
|
||||
positionerParameters.Offset = customPlacementParameters.Offset;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (target == null)
|
||||
throw new InvalidOperationException("Placement mode is not Pointer and PlacementTarget is null");
|
||||
Matrix? matrix;
|
||||
if (TryGetAdorner(target, out var adorned, out var adornerLayer))
|
||||
{
|
||||
matrix = adorned!.TransformToVisual(topLevel) * target.TransformToVisual(adornerLayer!);
|
||||
}
|
||||
else
|
||||
{
|
||||
matrix = target.TransformToVisual(topLevel);
|
||||
}
|
||||
positionerParameters.AnchorRectangle = CalculateAnchorRect(topLevel, positionRequest);
|
||||
|
||||
if (matrix == null)
|
||||
{
|
||||
if (target.GetVisualRoot() == null)
|
||||
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");
|
||||
}
|
||||
|
||||
var bounds = new Rect(default, target.Bounds.Size);
|
||||
var anchorRect = rect ?? bounds;
|
||||
positionerParameters.AnchorRectangle = anchorRect.Intersect(bounds).TransformToAABB(matrix.Value);
|
||||
|
||||
var parameters = placement switch
|
||||
var parameters = positionRequest.Placement switch
|
||||
{
|
||||
PlacementMode.Bottom => (PopupAnchor.Bottom, PopupGravity.Bottom),
|
||||
PlacementMode.Right => (PopupAnchor.Right, PopupGravity.Right),
|
||||
PlacementMode.Left => (PopupAnchor.Left, PopupGravity.Left),
|
||||
PlacementMode.Top => (PopupAnchor.Top, PopupGravity.Top),
|
||||
PlacementMode.Center => (PopupAnchor.None, PopupGravity.None),
|
||||
PlacementMode.AnchorAndGravity => (anchor, gravity),
|
||||
PlacementMode.AnchorAndGravity => (positionRequest.Anchor, positionRequest.Gravity),
|
||||
PlacementMode.TopEdgeAlignedRight => (PopupAnchor.TopRight, PopupGravity.TopLeft),
|
||||
PlacementMode.TopEdgeAlignedLeft => (PopupAnchor.TopLeft, PopupGravity.TopRight),
|
||||
PlacementMode.BottomEdgeAlignedLeft => (PopupAnchor.BottomLeft, PopupGravity.BottomRight),
|
||||
@@ -505,7 +530,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
|
||||
PlacementMode.LeftEdgeAlignedBottom => (PopupAnchor.BottomLeft, PopupGravity.TopLeft),
|
||||
PlacementMode.RightEdgeAlignedTop => (PopupAnchor.TopRight, PopupGravity.BottomRight),
|
||||
PlacementMode.RightEdgeAlignedBottom => (PopupAnchor.BottomRight, PopupGravity.TopRight),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(placement), placement,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(positionRequest.Placement), positionRequest.Placement,
|
||||
"Invalid value for Popup.PlacementMode")
|
||||
};
|
||||
positionerParameters.Anchor = parameters.Item1;
|
||||
@@ -537,6 +562,35 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
|
||||
positionerParameters.Gravity |= PopupGravity.Right;
|
||||
}
|
||||
}
|
||||
|
||||
return positionerParameters;
|
||||
}
|
||||
|
||||
private static Rect CalculateAnchorRect(TopLevel topLevel, PopupPositionRequest positionRequest)
|
||||
{
|
||||
var target = positionRequest.Target;
|
||||
if (target == null)
|
||||
throw new InvalidOperationException("Placement mode is not Pointer and PlacementTarget is null");
|
||||
Matrix? matrix;
|
||||
if (TryGetAdorner(target, out var adorned, out var adornerLayer))
|
||||
{
|
||||
matrix = adorned!.TransformToVisual(topLevel) * target.TransformToVisual(adornerLayer!);
|
||||
}
|
||||
else
|
||||
{
|
||||
matrix = target.TransformToVisual(topLevel);
|
||||
}
|
||||
|
||||
if (matrix == null)
|
||||
{
|
||||
if (target.GetVisualRoot() == null)
|
||||
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");
|
||||
}
|
||||
|
||||
var bounds = new Rect(default, target.Bounds.Size);
|
||||
var anchorRect = positionRequest.AnchorRect ?? bounds;
|
||||
return anchorRect.Intersect(bounds).TransformToAABB(matrix.Value);
|
||||
}
|
||||
|
||||
private static bool TryGetAdorner(Visual target, out Visual? adorned, out Visual? adornerLayer)
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
using Avalonia.Diagnostics;
|
||||
using Avalonia.Metadata;
|
||||
|
||||
namespace Avalonia.Controls.Primitives.PopupPositioning;
|
||||
|
||||
[PrivateApi]
|
||||
[Unstable(ObsoletionMessages.MayBeRemovedInAvalonia12)]
|
||||
public class PopupPositionRequest
|
||||
{
|
||||
internal PopupPositionRequest(Visual target, PlacementMode placement)
|
||||
{
|
||||
Target = target;
|
||||
Placement = placement;
|
||||
}
|
||||
|
||||
internal PopupPositionRequest(Visual target, PlacementMode placement, Point offset, PopupAnchor anchor, PopupGravity gravity, PopupPositionerConstraintAdjustment constraintAdjustment, Rect? anchorRect, CustomPopupPlacementCallback? placementCallback)
|
||||
: this(target, placement)
|
||||
{
|
||||
Offset = offset;
|
||||
Anchor = anchor;
|
||||
Gravity = gravity;
|
||||
ConstraintAdjustment = constraintAdjustment;
|
||||
AnchorRect = anchorRect;
|
||||
PlacementCallback = placementCallback;
|
||||
}
|
||||
|
||||
public Visual Target { get; }
|
||||
public PlacementMode Placement {get;}
|
||||
public Point Offset {get;}
|
||||
public PopupAnchor Anchor {get;}
|
||||
public PopupGravity Gravity {get;}
|
||||
public PopupPositionerConstraintAdjustment ConstraintAdjustment {get;}
|
||||
public Rect? AnchorRect {get;}
|
||||
public CustomPopupPlacementCallback? PlacementCallback {get;}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using Avalonia.Automation.Peers;
|
||||
using Avalonia.Controls.Primitives.PopupPositioning;
|
||||
using Avalonia.Diagnostics;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Metadata;
|
||||
using Avalonia.Platform;
|
||||
using Avalonia.Styling;
|
||||
using Avalonia.VisualTree;
|
||||
@@ -26,7 +28,9 @@ namespace Avalonia.Controls.Primitives
|
||||
public static readonly StyledProperty<bool> WindowManagerAddShadowHintProperty =
|
||||
Popup.WindowManagerAddShadowHintProperty.AddOwner<PopupRoot>();
|
||||
|
||||
private PopupPositionerParameters _positionerParameters;
|
||||
private PopupPositionRequest? _popupPositionRequest;
|
||||
private Size _popupSize;
|
||||
private bool _needsUpdate;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes static members of the <see cref="PopupRoot"/> class.
|
||||
@@ -124,20 +128,30 @@ namespace Avalonia.Controls.Primitives
|
||||
|
||||
private void UpdatePosition()
|
||||
{
|
||||
PlatformImpl?.PopupPositioner?.Update(_positionerParameters);
|
||||
if (_needsUpdate && _popupPositionRequest is not null)
|
||||
{
|
||||
_needsUpdate = false;
|
||||
PlatformImpl?.PopupPositioner?
|
||||
.Update(ParentTopLevel, _popupPositionRequest, _popupSize, FlowDirection);
|
||||
}
|
||||
}
|
||||
|
||||
[Unstable(ObsoletionMessages.MayBeRemovedInAvalonia12)]
|
||||
public void ConfigurePosition(Visual target, PlacementMode placement, Point offset,
|
||||
PopupAnchor anchor = PopupAnchor.None,
|
||||
PopupGravity gravity = PopupGravity.None,
|
||||
PopupPositionerConstraintAdjustment constraintAdjustment = PopupPositionerConstraintAdjustment.All,
|
||||
Rect? rect = null)
|
||||
{
|
||||
_positionerParameters.ConfigurePosition(ParentTopLevel, target,
|
||||
placement, offset, anchor, gravity, constraintAdjustment, rect, FlowDirection);
|
||||
((IPopupHost)this).ConfigurePosition(new PopupPositionRequest(target, placement, offset, anchor, gravity,
|
||||
constraintAdjustment, rect, null));
|
||||
}
|
||||
|
||||
if (_positionerParameters.Size != default)
|
||||
UpdatePosition();
|
||||
void IPopupHost.ConfigurePosition(PopupPositionRequest request)
|
||||
{
|
||||
_popupPositionRequest = request;
|
||||
_needsUpdate = true;
|
||||
UpdatePosition();
|
||||
}
|
||||
|
||||
public void SetChild(Control? control) => Content = control;
|
||||
@@ -184,10 +198,15 @@ namespace Avalonia.Controls.Primitives
|
||||
return new Size(width, height);
|
||||
}
|
||||
|
||||
protected override sealed Size ArrangeSetBounds(Size size)
|
||||
protected sealed override Size ArrangeSetBounds(Size size)
|
||||
{
|
||||
_positionerParameters.Size = size;
|
||||
UpdatePosition();
|
||||
if (_popupSize != size)
|
||||
{
|
||||
_popupSize = size;
|
||||
_needsUpdate = true;
|
||||
UpdatePosition();
|
||||
}
|
||||
|
||||
return ClientSize;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using Avalonia.Controls.Diagnostics;
|
||||
using Avalonia.Controls.Metadata;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Controls.Primitives.PopupPositioning;
|
||||
using Avalonia.Reactive;
|
||||
using Avalonia.Styling;
|
||||
|
||||
@@ -51,6 +52,10 @@ namespace Avalonia.Controls
|
||||
public static readonly AttachedProperty<double> VerticalOffsetProperty =
|
||||
AvaloniaProperty.RegisterAttached<ToolTip, Control, double>("VerticalOffset", 20);
|
||||
|
||||
/// <inheritdoc cref="Popup.CustomPopupPlacementCallbackProperty"/>
|
||||
public static readonly AttachedProperty<CustomPopupPlacementCallback?> CustomPopupPlacementCallbackProperty =
|
||||
AvaloniaProperty.RegisterAttached<ToolTip, Control, CustomPopupPlacementCallback?>("CustomPopupPlacementCallback");
|
||||
|
||||
/// <summary>
|
||||
/// Defines the ToolTip.ShowDelay property.
|
||||
/// </summary>
|
||||
@@ -327,6 +332,22 @@ namespace Avalonia.Controls
|
||||
public static void RemoveToolTipClosingHandler(Control element, EventHandler<RoutedEventArgs> handler) =>
|
||||
element.RemoveHandler(ToolTipClosingEvent, handler);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value of the ToolTip.CustomPopupPlacementCallback attached property.
|
||||
/// </summary>
|
||||
public static CustomPopupPlacementCallback? GetCustomPopupPlacementCallback(Control element)
|
||||
{
|
||||
return element.GetValue(CustomPopupPlacementCallbackProperty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the value of the ToolTip.CustomPopupPlacementCallback attached property.
|
||||
/// </summary>
|
||||
public static void SetCustomPopupPlacementCallback(Control element, CustomPopupPlacementCallback? value)
|
||||
{
|
||||
element.SetValue(CustomPopupPlacementCallbackProperty, value);
|
||||
}
|
||||
|
||||
private static void IsOpenChanged(AvaloniaPropertyChangedEventArgs e)
|
||||
{
|
||||
var control = (Control)e.Sender;
|
||||
@@ -397,7 +418,8 @@ namespace Avalonia.Controls
|
||||
{
|
||||
_popup.Bind(Popup.HorizontalOffsetProperty, control.GetBindingObservable(HorizontalOffsetProperty)),
|
||||
_popup.Bind(Popup.VerticalOffsetProperty, control.GetBindingObservable(VerticalOffsetProperty)),
|
||||
_popup.Bind(Popup.PlacementProperty, control.GetBindingObservable(PlacementProperty))
|
||||
_popup.Bind(Popup.PlacementProperty, control.GetBindingObservable(PlacementProperty)),
|
||||
_popup.Bind(Popup.CustomPopupPlacementCallbackProperty, control.GetBindingObservable(CustomPopupPlacementCallbackProperty))
|
||||
});
|
||||
|
||||
_popup.PlacementTarget = control;
|
||||
@@ -415,14 +437,14 @@ namespace Avalonia.Controls
|
||||
adornedControl.RaiseEvent(args);
|
||||
}
|
||||
|
||||
_subscriptions?.Dispose();
|
||||
|
||||
if (_popup is not null)
|
||||
{
|
||||
_popup.IsOpen = false;
|
||||
_popup.SetPopupParent(null);
|
||||
_popup.PlacementTarget = null;
|
||||
}
|
||||
|
||||
_subscriptions?.Dispose();
|
||||
}
|
||||
|
||||
private void OnPopupClosed(object? sender, EventArgs e)
|
||||
|
||||
@@ -262,6 +262,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
|
||||
var target = CreateTarget(window, popupImpl.Object);
|
||||
target.Content = child;
|
||||
|
||||
((IPopupHost)target).ConfigurePosition(new PopupPositionRequest(window, PlacementMode.Top));
|
||||
target.Show();
|
||||
|
||||
Assert.Equal(new Size(400, 1024), target.Bounds.Size);
|
||||
@@ -290,6 +291,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
|
||||
Height = 100,
|
||||
};
|
||||
|
||||
((IPopupHost)target).ConfigurePosition(new PopupPositionRequest(window, PlacementMode.Top));
|
||||
target.Show();
|
||||
|
||||
Assert.Equal(new Rect(0, 0, 400, 800), target.Bounds);
|
||||
@@ -313,6 +315,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
|
||||
target.Width = 400;
|
||||
target.Height = 800;
|
||||
|
||||
((IPopupHost)target).ConfigurePosition(new PopupPositionRequest(window, PlacementMode.Top));
|
||||
target.Show();
|
||||
|
||||
Assert.Equal(400, target.Width);
|
||||
|
||||
@@ -1206,6 +1206,50 @@ namespace Avalonia.Controls.UnitTests.Primitives
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Custom_Placement_Callback_Is_Executed()
|
||||
{
|
||||
using (CreateServices())
|
||||
{
|
||||
var callbackExecuted = 0;
|
||||
var popupContent = new Border { Width = 100, Height = 100 };
|
||||
var popup = new Popup
|
||||
{
|
||||
Child = popupContent,
|
||||
Placement = PlacementMode.Custom,
|
||||
HorizontalOffset = 42,
|
||||
VerticalOffset = 21
|
||||
};
|
||||
var popupParent = new Border { Child = popup };
|
||||
var root = PreparedWindow(popupParent);
|
||||
|
||||
popup.CustomPopupPlacementCallback = (parameters) =>
|
||||
{
|
||||
Assert.Equal(popupContent.Width, parameters.PopupSize.Width);
|
||||
Assert.Equal(popupContent.Height, parameters.PopupSize.Height);
|
||||
|
||||
Assert.Equal(root.Width, parameters.AnchorRectangle.Width);
|
||||
Assert.Equal(root.Height, parameters.AnchorRectangle.Height);
|
||||
|
||||
Assert.Equal(popup.HorizontalOffset, parameters.Offset.X);
|
||||
Assert.Equal(popup.VerticalOffset, parameters.Offset.Y);
|
||||
|
||||
callbackExecuted++;
|
||||
|
||||
parameters.Anchor = PopupAnchor.Top;
|
||||
parameters.Gravity = PopupGravity.Bottom;
|
||||
};
|
||||
|
||||
root.LayoutManager.ExecuteInitialLayoutPass();
|
||||
popup.Open();
|
||||
root.LayoutManager.ExecuteLayoutPass();
|
||||
|
||||
// Ideally, callback should be executed only once for this test.
|
||||
// But currently PlacementTargetLayoutUpdated triggers second update either way.
|
||||
Assert.Equal(2, callbackExecuted);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static PopupRoot CreateRoot(TopLevel popupParent, IPopupImpl impl = null)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user