using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using IFSEngine.Generation;
using IFSEngine.Model;
using IFSRenderer.AvaloniaShell.Rendering;
using IFSRenderer.AvaloniaShell.Controls;
namespace IFSRenderer.AvaloniaShell;
/// <summary>
/// Breeds random fractals. Candidates are pinned to keep them across batches, and a pinned
/// candidate can be sent to the editor without closing the window.
/// </summary>
/// <remarks>
/// <para>
/// A port of <c>WpfDisplay/Views/GeneratorWindow.xaml</c>: a 250px column of pinned fractals on
/// the left, mutation options above the results on the right, all three on md:Cards, with
/// 200x200 thumbnails.
/// </para>
/// <para>
/// The gesture model is the original's and differs from this port's first version: clicking a
/// GENERATED tile pins it, clicking a PINNED tile sends it to the editor, and the bin button on
/// a pinned tile unpins it. The window stays open throughout.
/// </para>
/// <para>
/// Thumbnails render on the shared GL thread one at a time and the open fractal is restored
/// afterwards, so a batch briefly pauses the main preview rather than needing a second renderer.
/// </para>
/// </remarks>
internal sealed class GeneratorWindow : Window
{
private const int ThumbSize = 200, ThumbQuality = 9;
private static readonly IBrush CardBrush = new SolidColorBrush(Color.Parse("#454545"));
private static readonly IBrush Paper = new SolidColorBrush(Color.Parse("#323232"));
private readonly IRenderQueue _service;
private readonly GeneratorOptions _options = new();
private readonly WrapPanel _generated = new() { Orientation = Orientation.Horizontal };
private readonly WrapPanel _pinned = new() { Orientation = Orientation.Horizontal };
private readonly TextBlock _status = new()
{
Foreground = new SolidColorBrush(Color.Parse("#ddffffff")),
VerticalAlignment = VerticalAlignment.Center,
Margin = new Thickness(8, 4, 0, 0),
};
private readonly ValueSlider _chance = new();
private readonly ValueSlider _strength = new();
private readonly ValueSlider _batch = new();
private readonly Button _newBatch = new();
private readonly List<IFS> _pinnedList = [];
private bool _busy;
private int _stamp;
/// <summary>Raised when a pinned candidate should be loaded into the editor.</summary>
public event Action<IFS> SendToMain;
/// <summary>Supplies the fractal currently open in the editor, for "Pin Main".</summary>
public Func<IFS> CurrentFractal { get; set; }
public GeneratorWindow(IRenderQueue service)
{
_service = service;
Title = "Generator";
Width = 1280;
Height = 720;
MinWidth = 900;
MinHeight = 560;
// NOT maximized here: setting WindowState before the window is mapped leaves it
// sized to its menu bar under X11, so the body never gets laid out. It is applied
// in Opened instead, once there is a real window to maximize.
Background = Paper;
FontSize = 13;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
var pinMain = new MenuItem { Header = "📌 Pin Main" };
ToolTip.SetTip(pinMain, "Pin the fractal that is loaded in the main window. (Ctrl+M)");
pinMain.Click += async (_, _) =>
{
IFS current = CurrentFractal?.Invoke();
if (current is not null)
await PinFractal(current);
};
var menu = new Menu { ItemsSource = new[] { pinMain } };
DockPanel.SetDock(menu, Dock.Top);
var body = new Grid
{
ColumnDefinitions = new ColumnDefinitions("250,*"),
MinHeight = 400,
};
Border pinnedCard = Card(Titled("📌 Pinned fractals", _pinned));
Grid.SetColumn(pinnedCard, 0);
body.Children.Add(pinnedCard);
var right = new Grid { RowDefinitions = new RowDefinitions("Auto,*") };
Border optionsCard = Card(BuildOptions());
Grid.SetRow(optionsCard, 0);
right.Children.Add(optionsCard);
Border resultsCard = Card(Titled("🎰 Generated fractals", _generated));
Grid.SetRow(resultsCard, 1);
right.Children.Add(resultsCard);
Grid.SetColumn(right, 1);
body.Children.Add(right);
var root = new DockPanel();
root.Children.Add(menu);
root.Children.Add(body);
Content = root;
Opened += async (_, _) =>
{
WindowState = WindowState.Maximized;
await GenerateBatch();
};
}
// ------------------------------------------------------------------ chrome
/// <summary>An md:Card: the measured #454545 surface, Margin 8 Padding 8.</summary>
private static Border Card(Control content) => new()
{
Background = CardBrush,
CornerRadius = new CornerRadius(4),
Margin = new Thickness(8),
Padding = new Thickness(8),
BoxShadow = BoxShadows.Parse("0 1 3 0 #55000000, 0 4 10 0 #33000000"),
Child = content,
};
/// <summary>A heading over a scrolling thumbnail area.</summary>
private static DockPanel Titled(string heading, Control content)
{
var label = new TextBlock { Text = heading, Margin = new Thickness(0, 0, 0, 6) };
DockPanel.SetDock(label, Dock.Top);
return new DockPanel
{
Children = { label, new ScrollViewer { Content = content } },
};
}
private static CheckBox Check(string label, bool value, Action<bool> set, bool enabled = true)
{
var c = new CheckBox
{
Content = label,
IsChecked = value,
IsEnabled = enabled,
Margin = new Thickness(4),
};
c.IsCheckedChanged += (_, _) => set(c.IsChecked == true);
return c;
}
private Control BuildOptions()
{
// Slider settings are GeneratorViewModel's, verbatim.
_chance.SliderSettings = new ValueSliderSettings
{
Label = "Mutation chance",
DefaultValue = 0.5,
MinValue = 0,
MaxValue = 1,
Increment = 0.01,
ValueChanged = v => _options.MutationChance = v,
};
_chance.Value = _options.MutationChance;
_strength.SliderSettings = new ValueSliderSettings
{
Label = "Mutation strength",
DefaultValue = 1.0,
MinValue = 0,
Increment = 0.1,
ValueChanged = v => _options.MutationStrength = v,
};
_strength.Value = _options.MutationStrength;
_batch.SliderSettings = new ValueSliderSettings
{
Label = "Batch size",
ToolTip = "The number of fractals to generate.",
DefaultValue = 30,
MinValue = 1,
MaxValue = 50,
Increment = 5,
ValueChanged = v => _options.BatchSize = (int)v,
};
_batch.Value = _options.BatchSize;
_batch.Width = 110;
_newBatch.Content = "🎲 New batch";
_newBatch.Padding = new Thickness(30, 0);
_newBatch.VerticalAlignment = VerticalAlignment.Bottom;
_newBatch.Margin = new Thickness(8, 0);
_newBatch.Click += async (_, _) => await GenerateBatch();
var columns = new Grid { ColumnDefinitions = new ColumnDefinitions("Auto,Auto,Auto,*,Auto") };
var col0 = new StackPanel
{
Margin = new Thickness(8),
Children =
{
Check("Iterators", _options.MutateIterators, v => _options.MutateIterators = v),
Check("Connections", _options.MutateConnections, v => _options.MutateConnections = v),
Check("Weights", _options.MutateConnectionWeights, v => _options.MutateConnectionWeights = v),
},
};
// GeneratorOptions.cs:19 replaced the boolean with a PaletteInterpolationMode; Mixbox is
// still the default, and the checkbox now selects between it and plain linear RGB.
CheckBox mixbox = Check("Pigment-based mixing",
_options.PaletteInterpolationMode == IFSEngine.Model.InterpolationMode.Mixbox,
v => _options.PaletteInterpolationMode = v
? IFSEngine.Model.InterpolationMode.Mixbox
: IFSEngine.Model.InterpolationMode.LinearRGB);
ToolTip.SetTip(mixbox, "Use Mixbox algorithm to mix colors similar to mixing paint.");
var col1 = new StackPanel
{
Margin = new Thickness(8),
Children =
{
Check("Palette", _options.MutatePalette, v => _options.MutatePalette = v),
Check("Coloring", _options.MutateColoring, v => _options.MutateColoring = v),
// md:ColorZone in the original: a tinted strip, not an interactive control.
new Border
{
Background = new SolidColorBrush(Color.Parse("#5a5a5a")),
Padding = new Thickness(6, 3),
Margin = new Thickness(4),
CornerRadius = new CornerRadius(2),
Child = new TextBlock { Text = "Dominant Color" },
},
mixbox,
},
};
// Transform selection is present but disabled in the original too - GeneratorOptions
// carries a "TODO: select transforms" where the field would go.
var col2 = new StackPanel
{
Margin = new Thickness(8),
Children =
{
Check("Transforms", false, _ => { }, enabled: false),
new ListBox { IsEnabled = false, MaxHeight = 60, ItemsSource = new[] { "Transforms" } },
Check("Parameters", _options.MutateParameters, v => _options.MutateParameters = v),
},
};
var col4 = new StackPanel
{
Margin = new Thickness(8),
Width = 190,
Children = { _chance, _strength },
};
Grid.SetColumn(col0, 0);
Grid.SetColumn(col1, 1);
Grid.SetColumn(col2, 2);
Grid.SetColumn(col4, 4);
columns.Children.Add(col0);
columns.Children.Add(col1);
columns.Children.Add(col2);
columns.Children.Add(col4);
var batchRow = new StackPanel
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Center,
Children = { _batch, _newBatch },
};
var heading = new TextBlock { Text = "🔮 Mutation options", Margin = new Thickness(0, 0, 0, 6) };
return new StackPanel { Children = { heading, columns, batchRow, _status } };
}
// -------------------------------------------------------------- generation
private async Task GenerateBatch()
{
if (_busy)
return;
_busy = true;
// The original swaps the button's content for an hourglass while it works.
_newBatch.Content = "⏳";
try
{
_generated.Children.Clear();
var generator = new Generator(_service.Transforms);
int count = Math.Max(_options.BatchSize, 1);
for (int i = 0; i < count; i++)
{
_status.Text = $"Rendering candidate {i + 1}/{count}…";
IFS fractal = generator.GenerateOne(_options);
fractal.Title = $"Generated {++_stamp}";
byte[] bgra = await _service.RenderThumbnailAsync(fractal, ThumbSize, ThumbSize, ThumbQuality);
_generated.Children.Add(Tile(ToBitmap(bgra), fractal, pinned: false));
}
_status.Text = $"{count} candidates";
}
catch (Exception ex)
{
_status.Text = "Generation failed: " + ex.Message;
}
finally
{
_newBatch.Content = "🎲 New batch";
_busy = false;
}
}
/// <summary>
/// Pins a candidate. The already-rendered bitmap is reused when there is one: re-rendering
/// produced a visibly DIFFERENT image, because each render accumulates its own samples, so
/// a pinned tile no longer matched the tile that was clicked.
/// </summary>
private async Task PinFractal(IFS fractal, WriteableBitmap existing = null)
{
if (_pinnedList.Contains(fractal))
return;
WriteableBitmap bitmap = existing;
if (bitmap is null)
{
byte[] bgra = await _service.RenderThumbnailAsync(fractal, ThumbSize, ThumbSize, ThumbQuality);
bitmap = ToBitmap(bgra);
}
_pinnedList.Add(fractal);
_pinned.Children.Add(Tile(bitmap, fractal, pinned: true));
}
/// <summary>Turns the renderer's bottom-up BGRA into a top-down bitmap.</summary>
private static WriteableBitmap ToBitmap(byte[] bgra)
{
var bitmap = new WriteableBitmap(new PixelSize(ThumbSize, ThumbSize),
new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Opaque);
using ILockedFramebuffer fb = bitmap.Lock();
int rowBytes = ThumbSize * 4;
for (int y = 0; y < ThumbSize; y++)
{
// GL reads bottom-up.
int srcOffset = (ThumbSize - 1 - y) * rowBytes;
Marshal.Copy(bgra, srcOffset, fb.Address + y * fb.RowBytes, rowBytes);
}
return bitmap;
}
/// <summary>
/// A 200x200 thumbnail. Generated tiles pin on click; pinned tiles send to the editor on
/// click and carry a small bin button to unpin.
/// </summary>
private Border Tile(WriteableBitmap bitmap, IFS fractal, bool pinned)
{
var overlay = new Grid { Width = ThumbSize, Height = ThumbSize };
overlay.Children.Add(new Image { Source = bitmap, Width = ThumbSize, Height = ThumbSize });
var host = new Border
{
Margin = new Thickness(4),
Child = overlay,
Cursor = new Cursor(StandardCursorType.Hand),
};
if (!pinned)
{
ToolTip.SetTip(host, "Pin this fractal");
host.PointerPressed += async (_, e) =>
{
if (e.GetCurrentPoint(host).Properties.IsLeftButtonPressed)
await PinFractal(fractal, bitmap);
};
return host;
}
var bin = new Button
{
Content = "🗑",
Width = 24,
Height = 24,
Padding = new Thickness(0),
FontSize = 11,
Margin = new Thickness(8),
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Bottom,
CornerRadius = new CornerRadius(12),
};
ToolTip.SetTip(bin, "Unpin");
bin.Click += (_, e) =>
{
_pinnedList.Remove(fractal);
_pinned.Children.Remove(host);
e.Handled = true;
};
overlay.Children.Add(bin);
ToolTip.SetTip(host, "Send to the editor");
host.PointerPressed += (_, e) =>
{
if (e.GetCurrentPoint(host).Properties.IsLeftButtonPressed)
SendToMain?.Invoke(fractal);
};
return host;
}
}