using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Media;
using IFSEngine.Model;
namespace IFSRenderer.AvaloniaShell.Controls;
/// <summary>
/// The palette as a gradient bar with a draggable swatch that shows the colour it is over.
/// </summary>
/// <remarks>
/// <para>
/// Port of <c>WpfDisplay/Views/PaletteSlider.xaml</c>, which is a <c>Slider</c> retemplated so
/// its track IS the palette gradient and its thumb is a 30x30 rectangle, stroked black at 2px
/// and 3px on hover, filled with the sampled colour. It is two-way bound to an iterator's
/// <c>ColorIndex</c>.
/// </para>
/// <para>
/// Without it an iterator's colour can only be chosen as an abstract 0-1 number with no
/// indication of which colour that lands on — the port had the palette as a read-only strip and
/// the index as a bare ValueSlider, so picking a hue was trial and error.
/// </para>
/// </remarks>
public sealed class PaletteSlider : Control
{
/// <summary>Thumb geometry, from PaletteSlider.xaml:40-45.</summary>
private const double ThumbSize = 30, StrokeNormal = 2, StrokeHover = 3;
/// <summary>Gradient stops sampled across the palette; enough to read as continuous.</summary>
private const int Stops = 96;
public static readonly StyledProperty<double> ColorIndexProperty =
AvaloniaProperty.Register<PaletteSlider, double>(nameof(ColorIndex),
defaultBindingMode: Avalonia.Data.BindingMode.TwoWay);
/// <summary>Position along the palette, 0 to 1.</summary>
public double ColorIndex
{
get => GetValue(ColorIndexProperty);
set => SetValue(ColorIndexProperty, value);
}
public static readonly StyledProperty<ColorPalette> PaletteProperty =
AvaloniaProperty.Register<PaletteSlider, ColorPalette>(nameof(Palette));
public ColorPalette Palette
{
get => GetValue(PaletteProperty);
set => SetValue(PaletteProperty, value);
}
/// <summary>Raised while the value is being dragged, so the host can apply it.</summary>
public event Action<double> ColorIndexChanged;
/// <summary>Raised once when a drag begins, for the undo snapshot.</summary>
public event Action DragStarted;
private bool _dragging;
private bool _hover;
static PaletteSlider()
{
AffectsRender<PaletteSlider>(ColorIndexProperty, PaletteProperty);
}
public PaletteSlider()
{
Height = ThumbSize + 4;
Cursor = new Cursor(StandardCursorType.SizeWestEast);
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
base.OnPointerPressed(e);
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
return;
_dragging = true;
DragStarted?.Invoke();
e.Pointer.Capture(this);
Apply(e.GetPosition(this).X);
e.Handled = true;
}
protected override void OnPointerMoved(PointerEventArgs e)
{
base.OnPointerMoved(e);
if (_dragging)
Apply(e.GetPosition(this).X);
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
base.OnPointerReleased(e);
if (!_dragging)
return;
_dragging = false;
e.Pointer.Capture(null);
}
protected override void OnPointerEntered(PointerEventArgs e)
{
base.OnPointerEntered(e);
_hover = true;
InvalidateVisual();
}
protected override void OnPointerExited(PointerEventArgs e)
{
base.OnPointerExited(e);
_hover = false;
InvalidateVisual();
}
/// <summary>Maps an x position to 0..1, keeping the thumb fully on the bar.</summary>
private void Apply(double x)
{
double usable = Math.Max(Bounds.Width - ThumbSize, 1);
double t = Math.Clamp((x - ThumbSize / 2) / usable, 0.0, 1.0);
ColorIndex = t;
ColorIndexChanged?.Invoke(t);
InvalidateVisual();
}
/// <summary>The palette colour at <paramref name="t"/>, or grey with no palette.</summary>
private Color Sample(double t)
{
ColorPalette palette = Palette;
if (palette is null || palette.KeyColors.Count == 0)
return Colors.Gray;
// ColorPalette samples through its own InterpolationMode rather than assuming linear RGB.
System.Numerics.Vector4 c = palette.SampleGradient(Math.Clamp(t, 0, 1));
return Color.FromRgb(Channel(c.X), Channel(c.Y), Channel(c.Z));
static byte Channel(float v) => (byte)Math.Clamp(v * 255f, 0f, 255f);
}
public override void Render(DrawingContext context)
{
base.Render(context);
if (Bounds.Width <= 0 || Bounds.Height <= 0)
return;
// The gradient runs the full width; the thumb travels inside it, so both use the same
// 0..1 domain and the swatch always shows the colour directly beneath it.
var brush = new LinearGradientBrush
{
StartPoint = new RelativePoint(0, 0.5, RelativeUnit.Relative),
EndPoint = new RelativePoint(1, 0.5, RelativeUnit.Relative),
};
for (int i = 0; i < Stops; i++)
{
double t = i / (double)(Stops - 1);
brush.GradientStops.Add(new GradientStop(Sample(t), t));
}
double barHeight = Math.Max(Bounds.Height - 8, 6);
var bar = new Rect(0, (Bounds.Height - barHeight) / 2, Bounds.Width, barHeight);
context.DrawRectangle(brush, null, bar, 3, 3);
double usable = Math.Max(Bounds.Width - ThumbSize, 1);
double centre = ThumbSize / 2 + Math.Clamp(ColorIndex, 0, 1) * usable;
var thumb = new Rect(centre - ThumbSize / 2, (Bounds.Height - ThumbSize) / 2,
ThumbSize, ThumbSize);
double stroke = _hover || _dragging ? StrokeHover : StrokeNormal;
context.DrawRectangle(
new SolidColorBrush(Sample(ColorIndex)),
new Pen(Brushes.Black, stroke),
thumb, 2, 2);
}
}