using System; using System.Diagnostics; using System.Reflection; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Media.Effects; namespace WPFluent.Controls.Primitives; public class ThemeShadowChrome : Decorator { static ThemeShadowChrome() { s_bg1 = new SolidColorBrush(Colors.Black) { Opacity = 0.11d }; s_bg2 = new SolidColorBrush(Colors.Black) { Opacity = 0.13d }; s_bg3 = new SolidColorBrush(Colors.Black) { Opacity = 0.18d }; s_bg4 = new SolidColorBrush(Colors.Black) { Opacity = 0.22d }; s_bg1.Freeze(); s_bg2.Freeze(); s_bg3.Freeze(); s_bg4.Freeze(); } public ThemeShadowChrome() { #if NET462_OR_NEWER _bitmapCache = new BitmapCache(VisualTreeHelper.GetDpi(this).PixelsPerDip); #else _bitmapCache = new BitmapCache(); #endif _background = new Grid { CacheMode = _bitmapCache, Focusable = false, IsHitTestVisible = false, SnapsToDevicePixels = false }; AddVisualChild(_background); SizeChanged += OnSizeChanged; Loaded += OnLoaded; } public static readonly DependencyProperty IsShadowEnabledProperty = DependencyProperty.Register(nameof(IsShadowEnabled), typeof(bool), typeof(ThemeShadowChrome), new PropertyMetadata(true, OnIsShadowEnabledChanged)); public bool IsShadowEnabled { get => (bool)GetValue(IsShadowEnabledProperty); set => SetValue(IsShadowEnabledProperty, value); } private static void OnIsShadowEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((ThemeShadowChrome)d).OnIsShadowEnabledChanged(); } private void OnIsShadowEnabledChanged() { if (IsInitialized) { if (IsShadowEnabled) { EnsureShadows(); Debug.Assert(_background.Children.Count == 0); _background.Children.Add(_shadow1); _background.Children.Add(_shadow2); _background.Visibility = Visibility.Visible; } else { _background.Children.Clear(); _background.Visibility = Visibility.Collapsed; } OnVisualParentChanged(); UpdatePopupMargin(); } } public static readonly DependencyProperty DepthProperty = DependencyProperty.Register( nameof(Depth), typeof(double), typeof(ThemeShadowChrome), new PropertyMetadata(32d, OnDepthChanged)); public double Depth { get => (double)GetValue(DepthProperty); set => SetValue(DepthProperty, value); } private static void OnDepthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((ThemeShadowChrome)d).OnDepthChanged(); } private void OnDepthChanged() { if (IsInitialized) { UpdateShadow1(); UpdateShadow2(); UpdatePopupMargin(); } } public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register(nameof(CornerRadius), typeof(CornerRadius), typeof(ThemeShadowChrome), new PropertyMetadata(new CornerRadius(), OnCornerRadiusChanged)); public CornerRadius CornerRadius { get => (CornerRadius)GetValue(CornerRadiusProperty); set => SetValue(CornerRadiusProperty, value); } private static void OnCornerRadiusChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((ThemeShadowChrome)d).OnCornerRadiusChanged(e); } private void OnCornerRadiusChanged(DependencyPropertyChangedEventArgs e) { var cornerRadius = (CornerRadius)e.NewValue; if (_shadow1 != null) { _shadow1.CornerRadius = cornerRadius; } if (_shadow2 != null) { _shadow2.CornerRadius = cornerRadius; } } private static readonly DependencyProperty PopupMarginProperty = DependencyProperty.Register(nameof(PopupMargin), typeof(Thickness), typeof(ThemeShadowChrome), new PropertyMetadata(new Thickness(), OnPopupMarginChanged)); private Thickness PopupMargin { get => (Thickness)GetValue(PopupMarginProperty); set => SetValue(PopupMarginProperty, value); } private static void OnPopupMarginChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((ThemeShadowChrome)d).OnPopupMarginChanged(e); } private void OnPopupMarginChanged(DependencyPropertyChangedEventArgs e) { ApplyPopupMargin(); } private void UpdatePopupMargin() { if (IsShadowEnabled) { var depth = Depth; var radius = 0.9d * depth; var offset = 0.4d * depth; PopupMargin = new Thickness( radius, radius, radius, radius + offset); } else { ClearValue(PopupMarginProperty); } } private void ApplyPopupMargin() { if (_parentPopupControl != null) { if (ReadLocalValue(PopupMarginProperty) == DependencyProperty.UnsetValue) { _parentPopupControl.ClearMargin(); } else { _parentPopupControl.SetMargin(PopupMargin); } } } protected override int VisualChildrenCount => IsShadowEnabled ? Child == null ? 1 : 2 : base.VisualChildrenCount; protected override void OnVisualParentChanged(DependencyObject oldParent) { base.OnVisualParentChanged(oldParent); if (IsInitialized) { OnVisualParentChanged(); } } protected override Visual GetVisualChild(int index) { if (IsShadowEnabled) { if (index == 0) { return _background; } else if (index == 1 && Child != null) { return Child; } throw new ArgumentOutOfRangeException(nameof(index)); } else { return base.GetVisualChild(index); } } protected override void OnInitialized(EventArgs e) { base.OnInitialized(e); OnIsShadowEnabledChanged(); } protected override Size MeasureOverride(Size constraint) { if (IsShadowEnabled) { _background.Measure(constraint); } return base.MeasureOverride(constraint); } protected override Size ArrangeOverride(Size arrangeSize) { if (IsShadowEnabled) { _background.Arrange(new Rect(arrangeSize)); } return base.ArrangeOverride(arrangeSize); } #if NET462_OR_NEWER protected override void OnDpiChanged(DpiScale oldDpi, DpiScale newDpi) { base.OnDpiChanged(oldDpi, newDpi); _bitmapCache.RenderAtScale = newDpi.PixelsPerDip; } #endif private void OnVisualParentChanged() { if (IsShadowEnabled) { PopupControl parentPopupControl = null!; var visualParent = VisualParent; if (visualParent is ContextMenu contextMenu) { parentPopupControl = new PopupControl(contextMenu); } else if (visualParent is ToolTip toolTip) { parentPopupControl = new PopupControl(toolTip); } else if (FindParentPopup(this) is Popup parentPopup) { parentPopupControl = new PopupControl(parentPopup); } SetParentPopupControl(parentPopupControl!); } else { SetParentPopupControl(null!); } } private void EnsureShadows() { if (_shadow1 == null) { _shadow1 = CreateShadowElement(); UpdateShadow1(); } if (_shadow2 == null) { _shadow2 = CreateShadowElement(); UpdateShadow2(); } } private Border CreateShadowElement() { return new Border { CornerRadius = CornerRadius, Effect = new BlurEffect(), RenderTransform = new TranslateTransform() }; } private void UpdateShadow1() { if (_shadow1 != null) { var depth = Depth; var effect = (BlurEffect)_shadow1.Effect; effect.Radius = 0.9 * depth; var transform = (TranslateTransform)_shadow1.RenderTransform; transform.Y = 0.4 * depth; _shadow1.Background = depth >= 32 ? s_bg4 : s_bg2; } } private void UpdateShadow2() { if (_shadow2 != null) { var depth = Depth; var effect = (BlurEffect)_shadow2.Effect; effect.Radius = 0.225 * depth; var transform = (TranslateTransform)_shadow2.RenderTransform; transform.Y = 0.075 * depth; _shadow2.Background = depth >= 32 ? s_bg3 : s_bg1; } } private void OnSizeChanged(object? sender, SizeChangedEventArgs e) { ClearMarginAdjustment(); UpdateLayout(); AdjustMargin(); } private void OnLoaded(object? sender, RoutedEventArgs e) { if (IsVisible) { AdjustMargin(); } } private void AdjustMargin() { if (_parentPopupControl != null) { var margin = Margin; if (margin != new Thickness() && VisualParent is UIElement parent) { var parentWidth = parent.RenderSize.Width; var shadowWidth = ActualWidth; if (parentWidth > 0 && shadowWidth > 0) { if (parentWidth < shadowWidth + margin.Left + margin.Right) { var leftRightMargin = (parentWidth - shadowWidth) / 2; var adjustedMargin = new Thickness(leftRightMargin, margin.Top, leftRightMargin, margin.Bottom); var marginAnim = new ThicknessAnimation(adjustedMargin, TimeSpan.Zero); BeginAnimation(MarginProperty, marginAnim); UpdateLayout(); } } } } } private void ClearMarginAdjustment() { BeginAnimation(MarginProperty, null); } private void SetParentPopupControl(PopupControl value) { if (_parentPopupControl == value) { return; } if (_popupPositioner != null) { _popupPositioner.Dispose(); _popupPositioner = null!; } if (_parentPopupControl != null) { _parentPopupControl.Opened -= OnParentPopupControlOpened; _parentPopupControl.Closed -= OnParentPopupControlClosed; _parentPopupControl.ClearMargin(); _parentPopupControl.Dispose(); } _parentPopupControl = value; if (_parentPopupControl != null) { _parentPopupControl.Opened += OnParentPopupControlOpened; _parentPopupControl.Closed += OnParentPopupControlClosed; ApplyPopupMargin(); } } private void OnParentPopupControlOpened(object? sender, EventArgs e) { if (_popupPositioner != null) { return; } if (_parentPopupControl != null) { if (_parentPopupControl.Control is { } control) { if (control is ToolTip toolTip && toolTip.PlacementTarget is Thumb thumb && thumb.TemplatedParent is Slider) { // Do not reposition slider auto tool tip return; } else { var popup = control as Popup ?? control.Parent as Popup; if (popup != null && PopupPositioner.IsSupported) { _popupPositioner = new PopupPositioner(popup); } } } } if (_popupPositioner == null) { PositionParentPopupControl(); } } private void OnParentPopupControlClosed(object? sender, EventArgs e) { ClearMarginAdjustment(); ResetTransform(); } private void PositionParentPopupControl() { var popup = _parentPopupControl; if (popup != null) { Debug.Assert(IsShadowEnabled); CustomPlacementMode? placement = null; switch (popup.Placement) { case PlacementMode.Bottom: placement = CustomPlacementMode.BottomEdgeAlignedLeft; break; case PlacementMode.Top: placement = CustomPlacementMode.TopEdgeAlignedLeft; break; case PlacementMode.Custom: if (TryGetCustomPlacementMode(out var customPlacement)) { placement = customPlacement; } break; } if (placement.HasValue) { if (!EnsureEdgesAligned(placement.Value)) { if (placement == CustomPlacementMode.BottomEdgeAlignedLeft) { if (shouldAlignRightEdges()) { EnsureEdgesAligned(CustomPlacementMode.BottomEdgeAlignedRight); } } else if (placement == CustomPlacementMode.TopEdgeAlignedLeft) { if (shouldAlignRightEdges()) { EnsureEdgesAligned(CustomPlacementMode.TopEdgeAlignedRight); } } } } bool shouldAlignRightEdges() { var target = popup.PlacementTarget; if (target != null) { var targetWidth = target.RenderSize.Width; if (ActualWidth > 0 && targetWidth > 0) { if (ActualWidth == targetWidth) { return true; } else if (ActualWidth > targetWidth) { if (TryGetOffsetToTarget(InterestPoint.TopRight, InterestPoint.TopRight, out var offset)) { if (offset.X < 0) { return true; } } } } } return false; } } } private bool TryGetCustomPlacementMode(out CustomPlacementMode placement) { if (TryGetCustomPlacementMode(_parentPopupControl?.Control!, out placement)) { return true; } if (TryGetCustomPlacementMode(VisualParent, out placement)) { return true; } return false; } private bool TryGetCustomPlacementMode(DependencyObject element, out CustomPlacementMode placement) { if (element != null && element.ReadLocalValue(CustomPopupPlacementHelper.PlacementProperty) != DependencyProperty.UnsetValue) { placement = CustomPopupPlacementHelper.GetPlacement(element); return true; } placement = default; return false; } private bool TryGetOffsetToTarget( InterestPoint targetInterestPoint, InterestPoint childInterestPoint, out Vector offset) { var popup = _parentPopupControl; if (popup != null) { var target = popup.PlacementTarget; if (target != null) { if (IsVisible && target.IsVisible) { offset = Helper.GetOffset(this, childInterestPoint, target, targetInterestPoint, popup.PlacementRectangle); if (Math.Abs(offset.X) < 0.5) { offset.X = 0; } if (Math.Abs(offset.Y) < 0.5) { offset.Y = 0; } return true; } } } offset = default; return false; } private bool EnsureEdgesAligned(CustomPlacementMode placement) { Vector offsetToTarget; var translation = s_noTranslation; switch (placement) { case CustomPlacementMode.TopEdgeAlignedLeft: if (TryGetOffsetToTarget(InterestPoint.TopLeft, InterestPoint.BottomLeft, out offsetToTarget)) { translation = getTranslation(true, true, offsetToTarget); } break; case CustomPlacementMode.TopEdgeAlignedRight: if (TryGetOffsetToTarget(InterestPoint.TopRight, InterestPoint.BottomRight, out offsetToTarget)) { translation = getTranslation(true, false, offsetToTarget); } break; case CustomPlacementMode.BottomEdgeAlignedLeft: if (TryGetOffsetToTarget(InterestPoint.BottomLeft, InterestPoint.TopLeft, out offsetToTarget)) { translation = getTranslation(false, true, offsetToTarget); } break; case CustomPlacementMode.BottomEdgeAlignedRight: if (TryGetOffsetToTarget(InterestPoint.BottomRight, InterestPoint.TopRight, out offsetToTarget)) { translation = getTranslation(false, false, offsetToTarget); } break; } if (translation != s_noTranslation) { SetupTransform(translation); return true; } else { ResetTransform(); return false; } Vector getTranslation(bool top, bool left, Vector offset) { double offsetX = 0; double offsetY = 0; if (left && offset.X > 0 || !left && offset.X < 0 || Math.Abs(offset.X) < 0.5) { offsetX = -offset.X; } if (top && offset.Y < PopupMargin.Top || !top && offset.Y > -PopupMargin.Bottom || Math.Abs(offset.Y) < 0.5) { offsetY = -offset.Y; } return new Vector(offsetX, offsetY); } } private void SetupTransform(Vector translation) { if (_transform == null) { _transform = new TranslateTransform(); RenderTransform = _transform; } _transform.X = translation.X; _transform.Y = translation.Y; } private void ResetTransform() { if (_transform != null) { _transform.ClearValue(TranslateTransform.XProperty); _transform.ClearValue(TranslateTransform.YProperty); } } private Popup FindParentPopup(FrameworkElement element) { var parent = element.Parent; if (parent is Popup popup) { return popup; } else if (parent is FrameworkElement fe) { return FindParentPopup(fe); } else { if (VisualTreeHelper.GetParent(element) is FrameworkElement visualParent) { return FindParentPopup(visualParent); } } return null!; } private class PopupControl : IDisposable { private ContextMenu _contextMenu = null!; private ToolTip _toolTip = null!; private Popup _popup = null!; public PopupControl(ContextMenu contextMenu) { _contextMenu = contextMenu; _contextMenu.Opened += OnOpened; _contextMenu.Closed += OnClosed; } public PopupControl(ToolTip toolTip) { _toolTip = toolTip; _toolTip.Opened += OnOpened; _toolTip.Closed += OnClosed; } public PopupControl(Popup popup) { _popup = popup; _popup.Opened += OnOpened; _popup.Closed += OnClosed; } public FrameworkElement Control => _contextMenu as FrameworkElement ?? _toolTip as FrameworkElement ?? _popup as FrameworkElement; public PlacementMode Placement { get { if (_contextMenu != null) { return _contextMenu.Placement; } if (_toolTip != null) { return _toolTip.Placement; } if (_popup != null) { return _popup.Placement; } return default; } } public UIElement PlacementTarget { get { if (_contextMenu != null) { return _contextMenu.PlacementTarget; } if (_toolTip != null) { return _toolTip.PlacementTarget; } if (_popup != null) { return (_popup.PlacementTarget ?? VisualTreeHelper.GetParent(_popup) as UIElement)!; } return null!; } } public Rect PlacementRectangle { get { if (_contextMenu != null) { return _contextMenu.PlacementRectangle; } if (_toolTip != null) { return _toolTip.PlacementRectangle; } if (_popup != null) { return _popup.PlacementRectangle; } return Rect.Empty; } } private FrameworkElement? ChildAsFE => _contextMenu as FrameworkElement ?? _toolTip as FrameworkElement ?? _popup?.Child as FrameworkElement; public event EventHandler? Opened; public event EventHandler? Closed; public void SetMargin(Thickness margin) { var child = ChildAsFE; if (child != null) { child.Margin = margin; } } public void ClearMargin() { ChildAsFE?.ClearValue(MarginProperty); } public void Dispose() { if (_contextMenu != null) { _contextMenu.Opened -= OnOpened; _contextMenu.Closed -= OnClosed; _contextMenu = null!; } else if (_toolTip != null) { _toolTip.Opened -= OnOpened; _toolTip.Closed -= OnClosed; _toolTip = null!; } else if (_popup != null) { _popup.Opened -= OnOpened; _popup.Closed -= OnClosed; _popup = null!; } } private void OnOpened(object? sender, EventArgs e) { Opened?.Invoke(this, e); } private void OnClosed(object? sender, EventArgs e) { Closed?.Invoke(this, e); } } private readonly Grid _background = null!; private readonly BitmapCache _bitmapCache = null!; private Border _shadow1 = null!; private Border _shadow2 = null!; private PopupControl _parentPopupControl = null!; private TranslateTransform _transform = null!; private PopupPositioner _popupPositioner = null!; private static readonly Brush s_bg1, s_bg2, s_bg3, s_bg4; private static readonly Vector s_noTranslation = new(0, 0); } internal static class PopupHelper { public static void Reposition(this Popup popup) { if (popup is null) { throw new ArgumentNullException(nameof(popup)); } if (PopupPositioner.GetPositioner(popup) is { } positioner) { positioner.Reposition(); } else { if (s_reposition.Value is { } reposition) { reposition(popup); } else { var offset = popup.HorizontalOffset; popup.SetCurrentValue(Popup.HorizontalOffsetProperty, offset + 0.1); popup.InvalidateProperty(Popup.HorizontalOffsetProperty); } } } private static Action CreateRepositionDelegate() { try { return DelegateHelper.CreateDelegate>( typeof(Popup), nameof(Reposition), BindingFlags.Instance | BindingFlags.NonPublic); } catch { return null!; } } private static readonly Lazy> s_reposition = new Lazy>(CreateRepositionDelegate); } file static class Helper { public static Vector GetOffset( UIElement element1, InterestPoint interestPoint1, UIElement element2, InterestPoint interestPoint2, Rect element2Bounds) { var point = element1.TranslatePoint(GetPoint(element1, interestPoint1), element2); if (element2Bounds.IsEmpty) { return point - GetPoint(element2, interestPoint2); } else { return point - GetPoint(element2Bounds, interestPoint2); } } private static Point GetPoint(UIElement element, InterestPoint interestPoint) { return GetPoint(new Rect(element.RenderSize), interestPoint); } private static Point GetPoint(Rect rect, InterestPoint interestPoint) { switch (interestPoint) { case InterestPoint.TopLeft: return rect.TopLeft; case InterestPoint.TopRight: return rect.TopRight; case InterestPoint.BottomLeft: return rect.BottomLeft; case InterestPoint.BottomRight: return rect.BottomRight; case InterestPoint.Center: return new Point(rect.Left + rect.Width / 2, rect.Top + rect.Height / 2); default: throw new ArgumentOutOfRangeException(nameof(interestPoint)); } } }