using AngleSharp.Dom;
using XFESystem.Desktop;
namespace XFESystem.Browser;
///
/// Immutable paint-only publication. No DOM node, layout box, text-metrics
/// provider or live canvas backing store crosses the worker/UI boundary.
///
public sealed class BrowserPaintSnapshot
{
private readonly IReadOnlyList commands;
private readonly BrowserCanvasSurface?[] surfaces;
private BrowserPaintSnapshot(BrowserViewport viewport, BrowserDrawCommand[] commands,
BrowserCanvasSurface?[] surfaces, double documentWidth, double documentHeight, long canvasBytes)
{
Viewport = viewport;
this.commands = Array.AsReadOnly(commands);
this.surfaces = surfaces;
DocumentWidth = documentWidth; DocumentHeight = documentHeight; CanvasBytes = canvasBytes;
}
public BrowserViewport Viewport { get; }
public IReadOnlyList Commands => commands;
public double DocumentWidth { get; }
public double DocumentHeight { get; }
public long CanvasBytes { get; }
/// Called only by the current exclusive page owner, before release publication.
public static BrowserPaintSnapshot Capture(BrowserDisplayList list,
Func? sourceSurfaces = null,
int maximumCommands = 100_000, long maximumCanvasPixels = 16_777_216)
{
ArgumentNullException.ThrowIfNull(list);
if (maximumCommands < 1 || maximumCommands > 100_000 || maximumCanvasPixels < 0 || maximumCanvasPixels > 16_777_216)
throw new ArgumentOutOfRangeException(nameof(maximumCommands), "Invalid paint snapshot limits.");
if (list.Commands.Count > maximumCommands) throw new BrowserLimitException("The paint snapshot command limit was exceeded.");
var commands = new BrowserDrawCommand[list.Commands.Count];
var surfaces = new BrowserCanvasSurface?[commands.Length];
var copied = new Dictionary();
long pixels = 0;
for (int index = 0; index < commands.Length; index++)
{
var source = list.Commands[index];
// Strings and value geometry are immutable. Every nested mutable
// list is independently copied; the node is deliberately dropped.
commands[index] = source with
{
Node = null,
Decoration = CopyDecoration(source.Decoration),
RoundedClip = source.RoundedClip is { } rounded ? rounded with { } : null
};
if (source.Kind != BrowserDrawKind.Canvas || source.Node is null || sourceSurfaces?.Invoke(source.Node) is not { } surface) continue;
if (!copied.TryGetValue(surface, out var frozen))
{
long size = checked((long)surface.Width * surface.Height);
if (surface.Width < 0 || surface.Height < 0 || size > maximumCanvasPixels - pixels)
throw new BrowserLimitException("The paint snapshot canvas pixel limit was exceeded.");
frozen = surface.CopyForPaintSnapshot();
copied.Add(surface, frozen); pixels += size;
}
surfaces[index] = frozen;
}
return new(list.Viewport, commands, surfaces, list.DocumentWidth, list.DocumentHeight, checked(pixels * sizeof(uint)));
}
private static BrowserBoxDecoration? CopyDecoration(BrowserBoxDecoration? source) => source is null ? null : source with
{
Shadows = Array.AsReadOnly(source.Shadows.Select(shadow => shadow with { }).ToArray()),
Gradients = Array.AsReadOnly(source.Gradients.Select(gradient => gradient with
{
Stops = Array.AsReadOnly(gradient.Stops.Select(stop => stop with { }).ToArray())
}).ToArray())
};
internal BrowserCanvasSurface? SurfaceAt(int commandIndex) => surfaces[commandIndex];
public void Paint(IDesktopCanvas canvas, DesktopBounds viewport) => BrowserRasterPainter.Paint(canvas, viewport, this);
}
public static partial class BrowserRasterPainter
{
/// Only the frozen snapshot and UI-owned renderer are accessed here.
public static void Paint(IDesktopCanvas canvas, DesktopBounds viewport, BrowserPaintSnapshot snapshot)
{
ArgumentNullException.ThrowIfNull(snapshot);
var web = canvas as IDesktopWebCanvas;
var rounded = new BrowserClipRasterCanvas(canvas);
var clips = new Stack();
var clip = new BrowserRect(viewport.X, viewport.Y, viewport.Width, viewport.Height);
if (canvas is IDesktopWebFrameCache frameCache)
{
var scene = frameCache.WebPaintClip;
clip = clip.Intersect(new(scene.X, scene.Y, scene.Width, scene.Height));
if (clip.Width <= 0 || clip.Height <= 0) return;
}
web?.PushClip(viewport);
canvas.Fill(0xFFFFFF, viewport.X, viewport.Y, viewport.Width, viewport.Height);
try
{
for (int index = 0; index < snapshot.Commands.Count; index++)
{
var item = snapshot.Commands[index];
if (item.Opacity <= 0 && item.Kind is not (BrowserDrawKind.PushClip or BrowserDrawKind.PopClip)) continue;
double dx = viewport.X - snapshot.Viewport.ScrollX, dy = viewport.Y - snapshot.Viewport.ScrollY;
var r = item.Bounds.Translate(dx, dy);
if (item.Kind == BrowserDrawKind.PushClip)
{ clips.Push(clip); clip = clip.Intersect(r); rounded.Push(item.RoundedClip, dx, dy); web?.PushClip(ToBounds(clip)); continue; }
if (item.Kind == BrowserDrawKind.PopClip)
{ if (clips.TryPop(out var old)) { clip = old; rounded.Pop(); web?.PopClip(); } continue; }
var visible = r.Intersect(clip);
if (visible.Width <= 0 || visible.Height <= 0) continue;
IDesktopCanvas target = rounded.Active && canvas is IDesktopCoverageCanvas ? rounded : canvas;
if (item.Decoration is not null) BrowserBoxRasterPainter.Paint(target, item, r, visible);
else if (item.Kind == BrowserDrawKind.Canvas)
{ if (snapshot.SurfaceAt(index) is { } surface) PaintCanvas(target, r, visible, surface, item.Opacity); }
else if (item.Kind == BrowserDrawKind.Text)
{
double leading = web is null ? 0 : (r.Height - web.MeasureWebLineHeight(item.Font.Size, item.Font.Family, item.Font.Weight)) / 2;
if (web is not null && rounded.Active && canvas is IDesktopCoverageCanvas) rounded.WebText(web, item, r.Translate(0, leading));
else if (web is not null) web.WebText(item.Text, item.Color, r.X, r.Y + leading, item.Font.Size, item.Font.Family, item.Font.Weight, item.Opacity);
else if (visible == r) canvas.Text(item.Text, item.Color, (int)r.X, (int)r.Y);
}
else
{
var b = ToBounds(visible);
if (item.Opacity < 1 && target is IDesktopCoverageCanvas coverage)
{
int left = (int)Math.Floor(visible.X * canvas.ScalePercent / 100d), top = (int)Math.Floor(visible.Y * canvas.ScalePercent / 100d);
int width = Math.Max(1, (int)Math.Ceiling(visible.Width * canvas.ScalePercent / 100d)), height = Math.Max(1, (int)Math.Ceiling(visible.Height * canvas.ScalePercent / 100d));
var row = new byte[width]; Array.Fill(row, (byte)Math.Clamp(item.Opacity * 255, 0, 255));
for (int y = 0; y < height; y++) coverage.BlendCoverage(item.Color, left, top + y, width, 1, row);
}
else target.Fill(item.Color, b.X, b.Y, b.Width, b.Height);
}
}
}
catch { while (clips.TryPop(out _)) web?.PopClip(); web?.PopClip(); throw; }
while (clips.TryPop(out _)) web?.PopClip(); web?.PopClip();
}
}