using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Layout;
using Avalonia.Media;
using IFSEngine.Model;
namespace IFSRenderer.AvaloniaShell.Controls;
/// <summary>
/// The searchable list of transform plugins, ported from the left column of
/// <c>WpfDisplay/Views/EditorWindow.xaml</c>.
/// </summary>
/// <remarks>
/// <para>
/// Clicking a transform ADDS an iterator using it — it does not retarget the selected one.
/// That is the canonical behaviour and the reason the original has no "change transform"
/// control anywhere.
/// </para>
/// <para>
/// The filter reproduces <c>IFSViewModel.FilterTransforms</c>, which is a ranking rather than
/// a predicate: names that START with the term come first, then names that CONTAIN it, then
/// matches on TAGS, de-duplicated in that order.
/// </para>
/// </remarks>
public sealed class TransformBrowser : UserControl
{
private readonly TextBox _filter = new()
{
Watermark = "Filter by name, tags",
Margin = new Thickness(0, 0, 0, 4),
};
private readonly StackPanel _list = new();
private IReadOnlyList<TransformPlugin> _all = [];
/// <summary>Raised when a transform is picked, to add an iterator using it.</summary>
public event Action<TransformPlugin> TransformActivated;
/// <summary>Raised by the 🔄 button.</summary>
public event Action ReloadRequested;
/// <summary>Raised by the 📂 button.</summary>
public event Action OpenDirectoryRequested;
/// <summary>Raised by a transform's ✏️ Edit source menu item.</summary>
public event Action<TransformPlugin> EditSourceRequested;
public TransformBrowser()
{
// Width comes from the grid column it sits in, so it stays resizable.
var title = new TextBlock
{
Text = "Transforms",
VerticalAlignment = VerticalAlignment.Center,
};
var reload = ToolButton("🔄", "Reload plugins (Ctrl+R)", () => ReloadRequested?.Invoke());
var folder = ToolButton("📂", "Open plugin directory", () => OpenDirectoryRequested?.Invoke());
var header = new Grid { ColumnDefinitions = new ColumnDefinitions("*,Auto") };
header.Children.Add(title);
var tools = new StackPanel { Orientation = Orientation.Horizontal, Children = { reload, folder } };
Grid.SetColumn(tools, 1);
header.Children.Add(tools);
DockPanel.SetDock(header, Dock.Top);
_filter.TextChanged += (_, _) => Rebuild();
DockPanel.SetDock(_filter, Dock.Top);
Content = new DockPanel
{
Margin = new Thickness(4, 0, 0, 0),
Children =
{
header,
_filter,
new ScrollViewer
{
HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled,
Content = _list,
},
},
};
}
private static Button ToolButton(string glyph, string tip, Action onClick)
{
var b = new Button
{
Content = glyph,
Classes = { "Flat" },
Background = Brushes.Transparent,
BorderThickness = new Thickness(0),
Padding = new Thickness(4, 0),
MinWidth = 0,
FontSize = 13,
};
ToolTip.SetTip(b, tip);
b.Click += (_, _) => onClick();
return b;
}
public void SetTransforms(IReadOnlyList<TransformPlugin> transforms)
{
_all = transforms ?? [];
Rebuild();
}
/// <summary>IFSViewModel.FilterTransforms — a ranked search, not a boolean filter.</summary>
private IEnumerable<TransformPlugin> Filtered()
{
string term = _filter.Text;
if (string.IsNullOrEmpty(term))
return _all;
var startsWith = _all.Where(t => t.Name.StartsWith(term, StringComparison.InvariantCultureIgnoreCase));
var contains = _all.Where(t => t.Name.Contains(term, StringComparison.InvariantCultureIgnoreCase));
var tagged = _all.Where(t => t.Tags.Any(tag => tag.Contains(term, StringComparison.InvariantCultureIgnoreCase)));
return startsWith.Concat(contains).Concat(tagged).Distinct();
}
private void Rebuild()
{
_list.Children.Clear();
foreach (TransformPlugin plugin in Filtered())
_list.Children.Add(Row(plugin));
}
/// <summary>
/// One transform: a flat full-width button showing the centred name, with a ⋮ that appears
/// on hover and opens the plugin's details.
/// </summary>
private Control Row(TransformPlugin plugin)
{
var name = new TextBlock
{
Text = plugin.Name,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Padding = new Thickness(24, 4),
};
var more = new Button
{
Content = "⋮",
Foreground = Brushes.Gray,
Background = Brushes.Transparent,
BorderThickness = new Thickness(0),
Padding = new Thickness(4, 0),
MinWidth = 0,
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Center,
IsVisible = false,
Flyout = DetailsFlyout(plugin),
};
var content = new Grid();
content.Children.Add(name);
content.Children.Add(more);
var button = new Button
{
Content = content,
Classes = { "Flat" },
Background = Brushes.Transparent,
BorderThickness = new Thickness(0),
CornerRadius = new CornerRadius(0),
Padding = new Thickness(0, 4),
HorizontalAlignment = HorizontalAlignment.Stretch,
HorizontalContentAlignment = HorizontalAlignment.Stretch,
};
button.Click += (_, _) => TransformActivated?.Invoke(plugin);
// The ⋮ only shows on hover, as in the original.
button.PointerEntered += (_, _) => more.IsVisible = true;
button.PointerExited += (_, _) => more.IsVisible = false;
return button;
}
/// <summary>
/// The plugin's details: name, tag chips, description, include-uses, a reference link and
/// Edit source — the same content as the WPF context menu.
/// </summary>
private Flyout DetailsFlyout(TransformPlugin plugin)
{
var stack = new StackPanel { MaxWidth = 260, Spacing = 4 };
stack.Children.Add(new TextBlock
{
Text = plugin.Name,
FontSize = 18,
FontWeight = FontWeight.Bold,
TextWrapping = TextWrapping.Wrap,
});
if (plugin.Tags is { Count: > 0 })
{
var chips = new WrapPanel { Orientation = Orientation.Horizontal };
foreach (string tag in plugin.Tags)
chips.Children.Add(new Border
{
Background = new SolidColorBrush(Color.Parse("#5a5a5a")),
CornerRadius = new CornerRadius(9),
Padding = new Thickness(8, 2),
Margin = new Thickness(0, 2, 4, 2),
Child = new TextBlock { Text = tag, FontSize = 11 },
});
stack.Children.Add(chips);
}
stack.Children.Add(Bold("Description"));
stack.Children.Add(new TextBlock
{
Text = string.IsNullOrWhiteSpace(plugin.Description) ? "—" : plugin.Description,
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(8, 0, 0, 0),
});
stack.Children.Add(Bold("Include uses"));
stack.Children.Add(new TextBlock
{
Text = plugin.IncludeUses is { Count: > 0 } ? string.Join(", ", plugin.IncludeUses) : "None",
TextWrapping = TextWrapping.Wrap,
Margin = new Thickness(8, 0, 0, 0),
});
if (plugin.ReferenceUrl is not null)
{
var info = new Button
{
Content = "ℹ️ Information…",
Classes = { "Flat" },
Background = Brushes.Transparent,
BorderThickness = new Thickness(0),
Padding = new Thickness(0, 4),
HorizontalAlignment = HorizontalAlignment.Left,
};
ToolTip.SetTip(info, plugin.ReferenceUrl.ToString());
info.Click += (_, _) => Dialogs.OpenUrl(plugin.ReferenceUrl.ToString());
stack.Children.Add(info);
}
var edit = new Button
{
Content = "✏️ Edit source",
Classes = { "Flat" },
Background = Brushes.Transparent,
BorderThickness = new Thickness(0),
Padding = new Thickness(0, 4),
HorizontalAlignment = HorizontalAlignment.Left,
};
edit.Click += (_, _) => EditSourceRequested?.Invoke(plugin);
stack.Children.Add(edit);
return new Flyout { Content = stack };
}
private static TextBlock Bold(string text) =>
new() { Text = text, FontWeight = FontWeight.Bold, Margin = new Thickness(0, 6, 0, 0) };
}