183 lines
6.1 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|