Files

183 lines
6.1 KiB
C#

using Microsoft.Maui.Controls.Shapes;
using Path = Microsoft.Maui.Controls.Shapes.Path;
namespace MarketAlly.SpotlightTour.Maui;
/// <summary>
/// An arrow indicator that points from the callout card toward the spotlight target.
/// </summary>
public class ArrowIndicator : ContentView
{
private readonly Path _arrowPath;
#region Bindable Properties
public static readonly BindableProperty ArrowColorProperty =
BindableProperty.Create(nameof(ArrowColor), typeof(Color), typeof(ArrowIndicator), Colors.White,
propertyChanged: (b, _, n) => ((ArrowIndicator)b).UpdateArrowColor((Color)n));
public Color ArrowColor
{
get => (Color)GetValue(ArrowColorProperty);
set => SetValue(ArrowColorProperty, value);
}
public static readonly BindableProperty ArrowSizeProperty =
BindableProperty.Create(nameof(ArrowSize), typeof(double), typeof(ArrowIndicator), 16.0);
public double ArrowSize
{
get => (double)GetValue(ArrowSizeProperty);
set => SetValue(ArrowSizeProperty, value);
}
public static readonly BindableProperty DirectionProperty =
BindableProperty.Create(nameof(Direction), typeof(CalloutPlacement), typeof(ArrowIndicator),
CalloutPlacement.Bottom,
propertyChanged: (b, _, _) => ((ArrowIndicator)b).UpdateArrow());
/// <summary>
/// The direction the arrow points (toward the target).
/// </summary>
public CalloutPlacement Direction
{
get => (CalloutPlacement)GetValue(DirectionProperty);
set => SetValue(DirectionProperty, value);
}
#endregion
public ArrowIndicator()
{
_arrowPath = new Path
{
Fill = new SolidColorBrush(ArrowColor),
Stroke = null
};
Content = _arrowPath;
UpdateArrow();
}
private void UpdateArrowColor(Color color)
{
// Ensure UI updates happen on the main thread
if (!MainThread.IsMainThread)
{
MainThread.BeginInvokeOnMainThread(() => UpdateArrowColor(color));
return;
}
_arrowPath.Fill = new SolidColorBrush(color);
}
private void UpdateArrow()
{
// Ensure UI updates happen on the main thread
if (!MainThread.IsMainThread)
{
MainThread.BeginInvokeOnMainThread(UpdateArrow);
return;
}
var size = ArrowSize;
var geometry = new PathGeometry();
var figure = new PathFigure { IsClosed = true };
switch (Direction)
{
case CalloutPlacement.Top:
// Arrow pointing up (callout is below target)
figure.StartPoint = new Point(size / 2, 0);
figure.Segments.Add(new LineSegment(new Point(size, size)));
figure.Segments.Add(new LineSegment(new Point(0, size)));
WidthRequest = size;
HeightRequest = size;
break;
case CalloutPlacement.Bottom:
// Arrow pointing down (callout is above target)
figure.StartPoint = new Point(0, 0);
figure.Segments.Add(new LineSegment(new Point(size, 0)));
figure.Segments.Add(new LineSegment(new Point(size / 2, size)));
WidthRequest = size;
HeightRequest = size;
break;
case CalloutPlacement.Left:
// Arrow pointing left (callout is to the right of target)
figure.StartPoint = new Point(0, size / 2);
figure.Segments.Add(new LineSegment(new Point(size, 0)));
figure.Segments.Add(new LineSegment(new Point(size, size)));
WidthRequest = size;
HeightRequest = size;
break;
case CalloutPlacement.Right:
// Arrow pointing right (callout is to the left of target)
figure.StartPoint = new Point(size, size / 2);
figure.Segments.Add(new LineSegment(new Point(0, 0)));
figure.Segments.Add(new LineSegment(new Point(0, size)));
WidthRequest = size;
HeightRequest = size;
break;
default:
// No arrow for Auto
IsVisible = false;
return;
}
geometry.Figures.Add(figure);
_arrowPath.Data = geometry;
IsVisible = true;
}
/// <summary>
/// Positions the arrow between the callout and spotlight.
/// </summary>
public void PositionBetween(Rect calloutBounds, Rect spotlightBounds, CalloutPlacement placement)
{
// Ensure UI updates happen on the main thread
if (!MainThread.IsMainThread)
{
MainThread.BeginInvokeOnMainThread(() => PositionBetween(calloutBounds, spotlightBounds, placement));
return;
}
Direction = placement;
var size = ArrowSize;
switch (placement)
{
case CalloutPlacement.Top:
// Callout is above, arrow points up from bottom of callout toward target
TranslationX = calloutBounds.Center.X - size / 2;
TranslationY = calloutBounds.Bottom;
break;
case CalloutPlacement.Bottom:
// Callout is below, arrow points down from top of callout toward target
TranslationX = calloutBounds.Center.X - size / 2;
TranslationY = calloutBounds.Top - size;
break;
case CalloutPlacement.Left:
// Callout is to the left, arrow points left from right of callout toward target
TranslationX = calloutBounds.Right;
TranslationY = calloutBounds.Center.Y - size / 2;
break;
case CalloutPlacement.Right:
// Callout is to the right, arrow points right from left of callout toward target
TranslationX = calloutBounds.Left - size;
TranslationY = calloutBounds.Center.Y - size / 2;
break;
default:
IsVisible = false;
break;
}
}
}